├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── Icon.png ├── IconSmall.png ├── License.md ├── NuGet README.md ├── README.md ├── screenshots ├── Jeremy Likness.png ├── Julie Lerman.png ├── Missing Index.PNG ├── Postgres Query Plan.PNG ├── Query Plan.PNG ├── QueryPlanNew.PNG └── Scott Hanselman.png └── src ├── LINQPadQueryPlanVisualizer.nuspec ├── QueryPlanVisualizer.LinqPad5 ├── Helpers │ ├── DatabaseHelper.cs │ ├── EntityFrameworkDatabaseHelper.cs │ └── LinqToSqlDatabaseHelper.cs ├── MissingIndexDetails.cs ├── NativeMethods.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── DataSources │ │ └── MissingIndexDetails.datasource │ ├── Resources.Designer.cs │ └── Resources.resx ├── QueryPlanProcessor.cs ├── QueryPlanUserControl.Designer.cs ├── QueryPlanUserControl.cs ├── QueryPlanUserControl.resx ├── QueryPlanVisualizer.LinqPad5.csproj ├── QueryPlanVisualizer.cs ├── Resources │ ├── jquery-1.12.1.min.js │ ├── qp.css │ ├── qp.js │ ├── qp.xslt │ ├── qp_icons.png │ ├── showplanxml.xsd │ └── template.html ├── XmlUtils.cs └── packages.config ├── QueryPlanVisualizer.LinqPad6 ├── DatabaseProvider.cs ├── Helpers │ ├── HttpClientExtensions.cs │ └── XmlUtils.cs ├── MissingIndexDetails.cs ├── MyLinkLabel.cs ├── NativeMethods.cs ├── OrmHelper.cs ├── PlanConvertor.cs ├── PostgresResources.Designer.cs ├── PostgresResources.resx ├── QueryPlanUserControl.Designer.cs ├── QueryPlanUserControl.cs ├── QueryPlanUserControl.resx ├── QueryPlanVisualizer.LinqPad6.csproj ├── QueryPlanVisualizer.cs ├── Resources │ ├── Postgres │ │ ├── all.css │ │ ├── app.css │ │ ├── app.js │ │ ├── bootstrap.min.css │ │ ├── chunk-vendors.js │ │ └── index.html │ └── SqlServer │ │ ├── qp.css │ │ ├── qp.min.js │ │ ├── qp_icons.png │ │ └── template.html ├── SqlServerResources.Designer.cs ├── SqlServerResources.resx └── TemporaryFiles.cs └── QueryPlanVisualizer.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | src/QueryPlanVisualizer.LinqPad6/Resources/**/*.css linguist-vendored -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Giorgi 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: giorgi 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | 49 | # Visual C++ cache files 50 | ipch/ 51 | *.aps 52 | *.ncb 53 | *.opensdf 54 | *.sdf 55 | *.cachefile 56 | 57 | # Visual Studio profiler 58 | *.psess 59 | *.vsp 60 | *.vspx 61 | 62 | # Guidance Automation Toolkit 63 | *.gpState 64 | 65 | # ReSharper is a .NET coding add-in 66 | _ReSharper*/ 67 | *.[Rr]e[Ss]harper 68 | 69 | # TeamCity is a build add-in 70 | _TeamCity* 71 | 72 | # DotCover is a Code Coverage Tool 73 | *.dotCover 74 | 75 | # NCrunch 76 | *.ncrunch* 77 | .*crunch*.local.xml 78 | 79 | # Installshield output folder 80 | [Ee]xpress/ 81 | 82 | # DocProject is a documentation generator add-in 83 | DocProject/buildhelp/ 84 | DocProject/Help/*.HxT 85 | DocProject/Help/*.HxC 86 | DocProject/Help/*.hhc 87 | DocProject/Help/*.hhk 88 | DocProject/Help/*.hhp 89 | DocProject/Help/Html2 90 | DocProject/Help/html 91 | 92 | # Click-Once directory 93 | publish/ 94 | 95 | # Publish Web Output 96 | *.Publish.xml 97 | 98 | # NuGet Packages Directory 99 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 100 | #packages/ 101 | 102 | # Windows Azure Build Output 103 | csx 104 | *.build.csdef 105 | 106 | # Windows Store app package directory 107 | AppPackages/ 108 | 109 | # Others 110 | sql/ 111 | *.Cache 112 | ClientBin/ 113 | [Ss]tyle[Cc]op.* 114 | ~$* 115 | *~ 116 | *.dbmdl 117 | *.publishsettings 118 | 119 | # RIA/Silverlight projects 120 | Generated_Code/ 121 | 122 | # Backup & report files from converting an old project file to a newer 123 | # Visual Studio version. Backup files are not needed, because we have git ;-) 124 | _UpgradeReport_Files/ 125 | Backup*/ 126 | UpgradeLog*.XML 127 | UpgradeLog*.htm 128 | 129 | # SQL Server files 130 | App_Data/*.mdf 131 | App_Data/*.ldf 132 | 133 | # ========================= 134 | # Windows detritus 135 | # ========================= 136 | 137 | # Windows image file caches 138 | Thumbs.db 139 | ehthumbs.db 140 | 141 | # Folder config file 142 | Desktop.ini 143 | 144 | # Recycle Bin used on file shares 145 | $RECYCLE.BIN/ 146 | 147 | # Mac crap 148 | .DS_Store 149 | 150 | 151 | .vs/ 152 | packages/ 153 | html-query-plan-2.6/ 154 | -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/Icon.png -------------------------------------------------------------------------------- /IconSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/IconSmall.png -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Giorgi Dalakishvili. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /NuGet README.md: -------------------------------------------------------------------------------- 1 | # LINQPad.QueryPlanVisualizer 2 | 3 | ## SQL Server and PostgreSQL query execution plan visualizer for LINQPad 4 | 5 | ## Features 6 | 7 | * View query execution plan inside LINQPad 8 | * View missing indexes for query 9 | * Share plan to [https://www.brentozar.com/pastetheplan/](https://www.brentozar.com/pastetheplan/) or [https://explain.dalibo.com/](https://explain.dalibo.com/) 10 | * Create missing indexes directly from LINQPad 11 | * Open plan in SQL Server Management Studio or another default app 12 | * Save plan to disk 13 | 14 | ## Getting Started 15 | 16 | **If you use LINQPad 6, you must use version 2.0 of this library. For LINQPad 5, you must use version 1.0** 17 | 18 | The library can show query plans for `LINQ to SQL` driver and `Entity Framework Core 5`. 19 | 20 | ### Install from NuGet 21 | 22 | If you have a Developer or higher edition of LINQPad, you can use the `LINQPadQueryPlanVisualizer` package from NuGet 23 | to add the visualizer to your queries. 24 | 25 | ### Install as plugin 26 | 27 | To install the visualizer as a LINQPad plugin, download the [latest release](https://github.com/Giorgi/QueryPlanVisualizer/releases/latest) and drop the visualizer dll directly inside LINQPad's plugins folder (by default found at **My Documents\LINQPad Plugins\NetCore3** for LINQPad 6 and **My Documents\LINQPad Plugins\Framework 4.6** for LINQPad 5). The plugin will be automatically available in all your queries. 28 | 29 | ## Viewing query plan 30 | 31 | To view query plan or missing indexes, call static `QueryPlanVisualizer.DumpPlan(query)` method or call `DumpPlan` extension method on an `IQueryable` instance. You will also need to add `ExecutionPlanVisualizer` to the namespaces list (click F4 to open the dialog). If you want to dump query result as well, pass `true` as a second parameter. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINQPad.QueryPlanVisualizer 2 | 3 | Visualize Entity Framework and Linq to SQL queries in LINQPad. For Visual Studio extension see [EFCore.Visualizer](https://github.com/Giorgi/EFCore.Visualizer) 4 | 5 | [![NuGet Package](https://img.shields.io/nuget/dt/LINQPadQueryPlanVisualizer.svg?label=LINQPadQueryPlanVisualizer&style=flat-square&logo=NuGet)](https://www.nuget.org/packages/LINQPadQueryPlanVisualizer/) 6 | [![GitHub all releases](https://img.shields.io/github/downloads/Giorgi/LINQPad.QueryPlanVisualizer/total?logo=github&style=flat-square)](https://github.com/Giorgi/LINQPad.QueryPlanVisualizer/releases) 7 | [![Apache License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square&logo=Apache)](License.md) 8 | [![Ko-Fi](https://img.shields.io/static/v1?style=flat-square&message=Support%20the%20Project&color=success&style=plastic&logo=ko-fi&label=$$)](https://ko-fi.com/U6U81LHU8) 9 | 10 | ## Entity Framework Community Standup Live Show 11 | 12 | [![Entity Framework Community Standup - Entity Framework Community Standup - Visualizing database query plans](https://img.youtube.com/vi/Zhy5antRDJk/0.jpg)](https://www.youtube.com/watch?v=Zhy5antRDJk) 13 | 14 | ## SQL Server and PostgreSQL query execution plan visualizer for LINQPad 15 | 16 | 17 | 18 | ## Features 19 | 20 | * View query execution plan inside LINQPad 21 | * View missing indexes for query 22 | * Share plan to [https://www.brentozar.com/pastetheplan/](https://www.brentozar.com/pastetheplan/) or [https://explain.dalibo.com/](https://explain.dalibo.com/) 23 | * Create missing indexes directly from LINQPad 24 | * Open plan in SQL Server Management Studio or another default app 25 | * Save plan to disk 26 | 27 | Supported databases: Sql Server and PostgreSQL. 28 | 29 | Supported ORMs: `Entity Framework Core 5` and `LINQ to SQL` 30 | 31 | ## Getting Started 32 | 33 | **If you use LINQPad 6 or newer, you must use version 2.X of this library. For LINQPad 5, you must use version 1.X** 34 | 35 | Version 2.1 and newer uses **Microsoft Edge WebView2** to display the query plan. This requires either **WebView2 Runtime** to be installed or a recent version of Edge Canary. To download WebView2 Runtime visit [Microsoft Edge WebView2 Download Page](https://developer.microsoft.com/en-us/microsoft-edge/webview2/). 36 | 37 | ### Install from NuGet 38 | 39 | If you have a Developer or higher edition of LINQPad, you can use the `LINQPadQueryPlanVisualizer` package from NuGet 40 | to add the visualizer to your queries. 41 | 42 | ### Install as plugin 43 | 44 | To install the visualizer as a LINQPad plugin, download the [latest release](https://github.com/Giorgi/QueryPlanVisualizer/releases/latest) and drop the visualizer dll directly inside LINQPad's plugins folder (by default found at **My Documents\LINQPad Plugins\NetCore3** for LINQPad 6 and **My Documents\LINQPad Plugins\Framework 4.6** for LINQPad 5). The plugin will be automatically available in all your queries. 45 | 46 | ## Viewing query plan 47 | 48 | To view query plan or missing indexes, call static `QueryPlanVisualizer.DumpPlan(query)` method or call `DumpPlan` extension method on an `IQueryable` instance. You will also need to add `ExecutionPlanVisualizer` to the namespaces list (click F4 to open the dialog). If you want to dump query result as well, pass `true` as a second parameter. 49 | 50 | Query execution plan for Sql Server: 51 | 52 | ![Sql Server query plan](screenshots/Query%20Plan.PNG "Query execution plan inside LINQPad") 53 | 54 | Query execution plan for PostgreSQL: 55 | 56 | ![PostgreSQL query plan](screenshots/Postgres%20Query%20Plan.PNG "Query execution plan inside LINQPad") 57 | 58 | ## Viewing missing indexes 59 | 60 | For SQL Server, the query plan can also return information about missing indexes in `QueryPlan/MissingIndexes/MissingIndexGroup` element. If missing indexes are present in the plan the visualizer will show a second tab with the missing index details and a button to create the index. 61 | 62 | Missing index: 63 | 64 | ![missing indexes](screenshots/Missing%20Index.PNG "Missing index") 65 | 66 | ## What Others Are Saying 67 | 68 | [![Scott Hanselman](screenshots/Scott%20Hanselman.png "Scott Hanselman")](https://twitter.com/shanselman/status/1555036430392389632) 69 | 70 | [![Julie Lerman](screenshots/Julie%20Lerman.png "Julie Lerman")](https://twitter.com/julielerman/status/1415367790844907527) 71 | 72 | [![Jeremy Likness](screenshots/Jeremy%20Likness.png "Jeremy Likness")](https://twitter.com/jeremylikness/status/1415368187760185346) 73 | -------------------------------------------------------------------------------- /screenshots/Jeremy Likness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/Jeremy Likness.png -------------------------------------------------------------------------------- /screenshots/Julie Lerman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/Julie Lerman.png -------------------------------------------------------------------------------- /screenshots/Missing Index.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/Missing Index.PNG -------------------------------------------------------------------------------- /screenshots/Postgres Query Plan.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/Postgres Query Plan.PNG -------------------------------------------------------------------------------- /screenshots/Query Plan.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/Query Plan.PNG -------------------------------------------------------------------------------- /screenshots/QueryPlanNew.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/QueryPlanNew.PNG -------------------------------------------------------------------------------- /screenshots/Scott Hanselman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/screenshots/Scott Hanselman.png -------------------------------------------------------------------------------- /src/LINQPadQueryPlanVisualizer.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | LINQPadQueryPlanVisualizer 5 | 1.2.0 6 | LINQPad Query Plan Visualizer 7 | Giorgi 8 | https://github.com/Giorgi/LINQPad.QueryPlanVisualizer/blob/master/License.md 9 | https://github.com/Giorgi/LINQPad.QueryPlanVisualizer 10 | false 11 | SQL Server query execution plan visualizer for LINQPad. 12 | 13 | Features Include: 14 | 15 | View query execution plan inside LINQPad 16 | View missing indexes for query 17 | Create missing indexes directly from LINQPad 18 | Open plan in SQL Server Management Studio or other default app 19 | Save plan to xml file 20 | SQL Server query execution plan visualizer for LINQPad 21 | 22 | Added support for showing plan from xml string 23 | 24 | Updated to version 1.1 of html-query-plan 25 | 26 | 27 | LINQPad SQL SQLServer 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Helpers/DatabaseHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Data.Common; 4 | using System.Data.Entity.Infrastructure; 5 | using System.Data.Linq; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | using LINQPad; 10 | 11 | namespace ExecutionPlanVisualizer.Helpers 12 | { 13 | internal abstract class DatabaseHelper 14 | { 15 | private DbConnection _dbConnection; 16 | 17 | public static DatabaseHelper Create(DataContextBase dataContextBase, IQueryable queryable) 18 | { 19 | if (dataContextBase != null) 20 | { 21 | return new LinqToSqlDatabaseHelper(dataContextBase); 22 | } 23 | 24 | var table = queryable as ITable; 25 | 26 | if (table != null) 27 | { 28 | return new LinqToSqlDatabaseHelper(table.Context); 29 | } 30 | 31 | var dataQueryType = typeof(DataContext).Assembly.GetType("System.Data.Linq.DataQuery`1"); 32 | 33 | if (queryable.GetType().GetGenericTypeDefinition() == dataQueryType) 34 | { 35 | var contextField = queryable.GetType().GetField("context", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField); 36 | var context = contextField?.GetValue(queryable) as DataContext; 37 | 38 | if (context != null) 39 | { 40 | return new LinqToSqlDatabaseHelper(context); 41 | } 42 | } 43 | 44 | return CreateEntityFrameworkDatabaseHelper(queryable); 45 | } 46 | 47 | private static DatabaseHelper CreateEntityFrameworkDatabaseHelper(IQueryable queryable) 48 | { 49 | var query = queryable as DbQuery; 50 | if (query != null) 51 | { 52 | var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetProperty; 53 | 54 | var internalQuery = query.GetType().GetProperty("InternalQuery", bindingFlags)?.GetValue(query); 55 | var objectQuery = 56 | internalQuery?.GetType().GetProperty("ObjectQuery")?.GetValue(internalQuery) as 57 | System.Data.Objects.ObjectQuery; 58 | 59 | if (objectQuery != null) 60 | //EF5 uses ObjectQuery from System.Data.Objects namespace, EF6 uses System.Data.Entity.Core.Objects so it will be null 61 | { 62 | return new EntityFramework5DatabaseHelper(objectQuery); 63 | } 64 | } 65 | 66 | return new EntityFrameworkDatabaseHelper(); 67 | } 68 | 69 | public DbConnection Connection 70 | { 71 | get 72 | { 73 | if (_dbConnection == null) 74 | { 75 | throw new InvalidOperationException("Connection has not been set."); 76 | } 77 | return _dbConnection; 78 | } 79 | set { _dbConnection = value; } 80 | } 81 | 82 | public virtual string GetSqlServerQueryExecutionPlan(IQueryable queryable) 83 | { 84 | using (var command = CreateCommand(queryable)) 85 | { 86 | try 87 | { 88 | if (Connection.State != ConnectionState.Open) 89 | { 90 | Connection.Open(); 91 | } 92 | 93 | using (var setStatisticsCommand = Connection.CreateCommand()) 94 | { 95 | setStatisticsCommand.CommandText = "SET STATISTICS XML ON"; 96 | setStatisticsCommand.ExecuteNonQuery(); 97 | } 98 | 99 | using (var reader = command.ExecuteReader()) 100 | { 101 | while (reader.NextResult()) 102 | { 103 | if (reader.GetName(0) == "Microsoft SQL Server 2005 XML Showplan") 104 | { 105 | reader.Read(); 106 | return reader.GetString(0); 107 | } 108 | } 109 | } 110 | 111 | return null; 112 | } 113 | finally 114 | { 115 | Connection.Close(); 116 | } 117 | } 118 | } 119 | 120 | public virtual async Task CreateIndexAsync(string script) 121 | { 122 | try 123 | { 124 | await Connection.OpenAsync(); 125 | 126 | using (var command = Connection.CreateCommand()) 127 | { 128 | command.CommandText = script; 129 | var result = await command.ExecuteNonQueryAsync(); 130 | } 131 | } 132 | finally 133 | { 134 | Connection.Close(); 135 | } 136 | } 137 | 138 | protected abstract DbCommand CreateCommand(IQueryable queryable); 139 | } 140 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Helpers/EntityFrameworkDatabaseHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Data.Entity.Infrastructure.Interception; 4 | using System.Data.EntityClient; 5 | using System.Data.Objects; 6 | using System.Linq; 7 | 8 | namespace ExecutionPlanVisualizer.Helpers 9 | { 10 | class EntityFramework5DatabaseHelper : DatabaseHelper 11 | { 12 | private ObjectParameterCollection parameters; 13 | 14 | public EntityFramework5DatabaseHelper(ObjectQuery objectQuery) 15 | { 16 | Connection = (objectQuery.Context.Connection as EntityConnection)?.StoreConnection; 17 | parameters = objectQuery.Parameters; 18 | } 19 | 20 | protected override DbCommand CreateCommand(IQueryable queryable) 21 | { 22 | var command = Connection.CreateCommand(); 23 | command.CommandText = queryable.ToString(); 24 | 25 | var copiedParameters = parameters.Select(parameter => 26 | { 27 | var parameterCopy = command.CreateParameter(); 28 | parameterCopy.ParameterName = parameter.Name; 29 | parameterCopy.Value = parameter.Value; 30 | return parameterCopy; 31 | }).ToArray(); 32 | 33 | command.Parameters.AddRange(copiedParameters); 34 | 35 | return command; 36 | } 37 | } 38 | 39 | internal class EntityFrameworkDatabaseHelper : DatabaseHelper 40 | { 41 | protected override DbCommand CreateCommand(IQueryable queryable) 42 | { 43 | var interceptor = new CommandCapturingInterceptor(); 44 | 45 | DbInterception.Add(interceptor); 46 | 47 | try 48 | { 49 | var result = queryable.Provider.Execute(queryable.Expression); 50 | } 51 | catch (Exception ex) when (ex is CommandCapturedException || ex.InnerException is CommandCapturedException) 52 | { 53 | } 54 | finally 55 | { 56 | DbInterception.Remove(interceptor); 57 | } 58 | 59 | if (interceptor.Command == null) 60 | { 61 | throw new InvalidOperationException("DbInterception failed to capture DbCommand."); 62 | } 63 | 64 | Connection = interceptor.Command.Connection; 65 | 66 | var command = Connection.CreateCommand(); 67 | 68 | command.CommandText = interceptor.Command.CommandText; 69 | var copiedParameters = interceptor.Command.Parameters.OfType() 70 | .Select(parameter => 71 | { 72 | var parameterCopy = command.CreateParameter(); 73 | parameterCopy.ParameterName = parameter.ParameterName; 74 | parameterCopy.DbType = parameter.DbType; 75 | parameterCopy.Value = parameter.Value; 76 | return parameterCopy; 77 | }).ToArray(); 78 | 79 | command.Parameters.AddRange(copiedParameters); 80 | 81 | return command; 82 | } 83 | 84 | private class CommandCapturedException : Exception { } 85 | 86 | private sealed class CommandCapturingInterceptor : IDbCommandInterceptor 87 | { 88 | public DbCommand Command { get; private set; } 89 | 90 | public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) 91 | { 92 | } 93 | 94 | public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) 95 | { 96 | } 97 | 98 | public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) 99 | { 100 | Command = command; 101 | interceptionContext.Exception = new CommandCapturedException(); 102 | } 103 | 104 | public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) 105 | { 106 | } 107 | 108 | public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) 109 | { 110 | } 111 | 112 | public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) 113 | { 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Helpers/LinqToSqlDatabaseHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Data.Linq; 3 | using System.Linq; 4 | 5 | namespace ExecutionPlanVisualizer.Helpers 6 | { 7 | internal class LinqToSqlDatabaseHelper : DatabaseHelper 8 | { 9 | private readonly DataContext dataContext; 10 | 11 | public LinqToSqlDatabaseHelper(DataContext dataContext) 12 | { 13 | this.dataContext = dataContext; 14 | Connection = dataContext.Connection; 15 | } 16 | 17 | protected override DbCommand CreateCommand(IQueryable queryable) 18 | { 19 | return dataContext.GetCommand(queryable); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/MissingIndexDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ExecutionPlanVisualizer 6 | { 7 | public class MissingIndexDetails 8 | { 9 | private const string CreateIndexTemplate = "CREATE NONCLUSTERED INDEX [IX_{0}_{1:yyyyMMdd_HHmmss_fff}] ON {2}.{3}.{4} ({5})"; 10 | private readonly Lazy scriptGenerator; 11 | 12 | public MissingIndexDetails() 13 | { 14 | scriptGenerator = new Lazy(CreateScript); 15 | } 16 | 17 | public double Impact { get; set; } 18 | 19 | public string Database { get; set; } 20 | public string Schema { get; set; } 21 | public string Table { get; set; } 22 | 23 | public List EqualityColumns { get; set; } 24 | public List InequalityColumns { get; set; } 25 | 26 | public List IncludeColumns { get; set; } 27 | 28 | public string Script => scriptGenerator.Value; 29 | 30 | private string CreateScript() 31 | { 32 | var indexColumns = string.Join(",", EqualityColumns.Concat(InequalityColumns)); 33 | 34 | var script = string.Format(CreateIndexTemplate, Table.Trim('[', ']'), DateTime.UtcNow, Database, Schema, Table, 35 | indexColumns); 36 | 37 | if (IncludeColumns?.Count > 0) 38 | { 39 | var includeColumns = string.Join(",", IncludeColumns); 40 | script += $" INCLUDE ({includeColumns})"; 41 | } 42 | 43 | return script; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace ExecutionPlanVisualizer 6 | { 7 | class NativeMethods 8 | { 9 | [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)] 10 | private static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, ref uint pcchOut); 11 | 12 | public static string AssocQueryString(AssocStr association, string extension) 13 | { 14 | const int S_OK = 0; 15 | const int S_FALSE = 1; 16 | 17 | uint length = 0; 18 | var result = AssocQueryString(AssocF.None, association, extension, null, null, ref length); 19 | if (result != S_FALSE) 20 | { 21 | return null; 22 | } 23 | 24 | var stringBuilder = new StringBuilder((int)length); 25 | result = AssocQueryString(AssocF.None, association, extension, null, stringBuilder, ref length); 26 | if (result != S_OK) 27 | { 28 | return null; 29 | } 30 | 31 | return stringBuilder.ToString(); 32 | } 33 | 34 | [Flags] 35 | enum AssocF : uint 36 | { 37 | None = 0, 38 | Init_NoRemapCLSID = 0x1, 39 | Init_ByExeName = 0x2, 40 | Open_ByExeName = 0x2, 41 | Init_DefaultToStar = 0x4, 42 | Init_DefaultToFolder = 0x8, 43 | NoUserSettings = 0x10, 44 | NoTruncate = 0x20, 45 | Verify = 0x40, 46 | RemapRunDll = 0x80, 47 | NoFixUps = 0x100, 48 | IgnoreBaseClass = 0x200, 49 | Init_IgnoreUnknown = 0x400, 50 | Init_FixedProgId = 0x800, 51 | IsProtocol = 0x1000, 52 | InitForFile = 0x2000, 53 | } 54 | 55 | internal enum AssocStr 56 | { 57 | Command = 1, 58 | Executable, 59 | FriendlyDocName, 60 | FriendlyAppName, 61 | NoOpen, 62 | ShellNewValue, 63 | DDECommand, 64 | DDEIfExec, 65 | DDEApplication, 66 | DDETopic, 67 | InfoTip, 68 | QuickTip, 69 | TileInfo, 70 | ContentType, 71 | DefaultIcon, 72 | ShellExtension, 73 | DropTarget, 74 | DelegateExecute, 75 | SupportedUriProtocols, 76 | Max, 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("QueryPlanVisualizer")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Giorgi Dalakishvili")] 11 | [assembly: AssemblyProduct("QueryPlanVisualizer")] 12 | [assembly: AssemblyCopyright("Copyright © Giorgi Dalakishvili 2016")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("df42bbc7-9733-4fd3-9758-13a1f657cfbb")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.2.1.0")] 35 | [assembly: AssemblyFileVersion("1.2.1.0")] 36 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Properties/DataSources/MissingIndexDetails.datasource: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | ExecutionPlanVisualizer.MissingIndexDetails, LINQPad.QueryPlanVisualizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 10 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ExecutionPlanVisualizer.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ExecutionPlanVisualizer.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to /*! jQuery v1.12.1 | (c) jQuery Foundation | jquery.org/license */ 65 | ///!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+ [rest of string was truncated]";. 66 | /// 67 | internal static string jquery { 68 | get { 69 | return ResourceManager.GetString("jquery", resourceCulture); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Bitmap. 75 | /// 76 | internal static System.Drawing.Bitmap qp_icons { 77 | get { 78 | object obj = ResourceManager.GetObject("qp_icons", resourceCulture); 79 | return ((System.Drawing.Bitmap)(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// Looks up a localized string similar to (function webpackUniversalModuleDefinition(root, factory) { 85 | /// if(typeof exports === 'object' && typeof module === 'object') 86 | /// module.exports = factory(); 87 | /// else if(typeof define === 'function' && define.amd) 88 | /// define([], factory); 89 | /// else if(typeof exports === 'object') 90 | /// exports["QP"] = factory(); 91 | /// else 92 | /// root["QP"] = factory(); 93 | ///})(this, function() { 94 | ///return /******/ (function(modules) { // webpackBootstrap 95 | ////******/ // The module cache 96 | ////******/ var installedModules = {}; 97 | /// 98 | ////******/ // The require function 99 | ////** [rest of string was truncated]";. 100 | /// 101 | internal static string qpJavascript { 102 | get { 103 | return ResourceManager.GetString("qpJavascript", resourceCulture); 104 | } 105 | } 106 | 107 | /// 108 | /// Looks up a localized string similar to div.qp-node { 109 | /// background-color: #FFFFCC; 110 | /// margin: 2px; 111 | /// padding: 2px; 112 | /// border: 1px solid black; 113 | /// font-size: 11px; 114 | /// line-height: normal; 115 | ///} 116 | /// 117 | ///.qp-node>div { 118 | /// font-family: Monospace; 119 | /// text-align: center; 120 | ///} 121 | /// 122 | ///div[class|='qp-icon'] { 123 | /// height: 32px; 124 | /// width: 32px; 125 | /// margin-left: auto; 126 | /// margin-right: auto; 127 | /// background-repeat: no-repeat; 128 | ///} 129 | /// 130 | ///.qp-tt { 131 | /// top: 4em; 132 | /// left: 2em; 133 | /// border: 1px solid black; 134 | /// background-color: #FFFFEE; 135 | /// padding: 2px [rest of string was truncated]";. 136 | /// 137 | internal static string qpStyleSheet { 138 | get { 139 | return ResourceManager.GetString("qpStyleSheet", resourceCulture); 140 | } 141 | } 142 | 143 | /// 144 | /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> 145 | ///<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 146 | /// xmlns:msxsl="urn:schemas-microsoft-com:xslt" 147 | /// xmlns:exslt="http://exslt.org/common" 148 | /// xmlns:s="http://schemas.microsoft.com/sqlserver/2004/07/showplan" 149 | /// exclude-result-prefixes="msxsl s xsl"> 150 | /// <xsl:output method="html" indent="no" omit-xml-declaration="yes" /> 151 | /// 152 | /// <!-- Disable built-in recursive processing templates --> 153 | /// <xsl:template match="*|/|text()|@*" mode="NodeLabel2" /> 154 | /// <xsl:te [rest of string was truncated]";. 155 | /// 156 | internal static string qpXslt { 157 | get { 158 | return ResourceManager.GetString("qpXslt", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> 164 | ///<xsd:schema targetNamespace="http://schemas.microsoft.com/sqlserver/2004/07/showplan" xmlns:shp="http://schemas.microsoft.com/sqlserver/2004/07/showplan" xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified" version="1.3" blockDefault="#all"> 165 | /// <xsd:annotation> 166 | /// <xsd:documentation> 167 | /// The following schema for Microsoft SQL Server describes output from the 168 | /// showplan functionality in XML format. 169 | /// 170 | /// [rest of string was truncated]";. 171 | /// 172 | internal static string showplanxml { 173 | get { 174 | return ResourceManager.GetString("showplanxml", resourceCulture); 175 | } 176 | } 177 | 178 | /// 179 | /// Looks up a localized string similar to <!DOCTYPE HTML> 180 | ///<html> 181 | ///<head> 182 | /// <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> 183 | /// <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 184 | /// <link type="text/css" media="all" rel="stylesheet" href="{0}" /> 185 | /// <script type="text/javascript" src="{1}"></script> 186 | /// <script src="{2}"></script> 187 | ///</head> 188 | ///<body> 189 | /// {3} 190 | /// <script type="text/javascript"> 191 | /// $(document).ready(function() {{ 192 | /// QP.drawLines($(".qp-root")); 193 | /// }}); 194 | /// </script> 195 | ///</ [rest of string was truncated]";. 196 | /// 197 | internal static string template { 198 | get { 199 | return ResourceManager.GetString("template", resourceCulture); 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | ..\Resources\jquery-1.12.1.min.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 123 | 124 | 125 | ..\Resources\qp.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 126 | 127 | 128 | ..\Resources\qp.css;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 129 | 130 | 131 | ..\Resources\qp.xslt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 132 | 133 | 134 | ..\Resources\qp_icons.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 135 | 136 | 137 | ..\Resources\showplanxml.xsd;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 138 | 139 | 140 | ..\Resources\template.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 141 | 142 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/QueryPlanProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Xml; 7 | using System.Xml.Linq; 8 | using System.Xml.Schema; 9 | using System.Xml.Xsl; 10 | using ExecutionPlanVisualizer.Properties; 11 | 12 | namespace ExecutionPlanVisualizer 13 | { 14 | class QueryPlanProcessor 15 | { 16 | private readonly string planXml; 17 | private static readonly XNamespace PlanXmlNamespace = "http://schemas.microsoft.com/sqlserver/2004/07/showplan"; 18 | 19 | public QueryPlanProcessor(string planXml) 20 | { 21 | this.planXml = planXml; 22 | } 23 | 24 | public List GetMissingIndexes() 25 | { 26 | var document = XDocument.Parse(planXml); 27 | 28 | var missingIndexGroups = document.Descendants(PlanXmlNamespace.WithName("MissingIndexGroup")); 29 | 30 | var result = from missingIndexGroup in missingIndexGroups 31 | 32 | let missingIndexes = missingIndexGroup.Descendants(PlanXmlNamespace.WithName("MissingIndex")) 33 | 34 | let indexes = from missingIndex in missingIndexes 35 | let columnGroups = missingIndex.Descendants(PlanXmlNamespace.WithName("ColumnGroup")) 36 | 37 | let equalityColumns = (from columnGroup in columnGroups 38 | where columnGroup.AttributeValue("Usage") == "EQUALITY" 39 | from column in columnGroup.Descendants() 40 | select column.AttributeValue("Name")) 41 | 42 | let inequalityColumns = (from columnGroup in columnGroups 43 | where columnGroup.AttributeValue("Usage") == "INEQUALITY" 44 | from column in columnGroup.Descendants() 45 | select column.AttributeValue("Name")) 46 | 47 | let includeColumns = (from columnGroup in columnGroups 48 | where columnGroup.AttributeValue("Usage") == "INCLUDE" 49 | from column in columnGroup.Descendants() 50 | select column.AttributeValue("Name")) 51 | 52 | select new MissingIndexDetails 53 | { 54 | Impact = Convert.ToDouble(missingIndexGroup.AttributeValue("Impact")), 55 | 56 | Database = missingIndex.AttributeValue("Database"), 57 | Table = missingIndex.AttributeValue("Table"), 58 | Schema = missingIndex.AttributeValue("Schema"), 59 | 60 | EqualityColumns = new List(equalityColumns), 61 | InequalityColumns = new List(inequalityColumns), 62 | 63 | IncludeColumns = new List(includeColumns) 64 | } 65 | 66 | from index in indexes 67 | select index; 68 | 69 | return result.ToList(); 70 | } 71 | 72 | public string ConvertPlanToHtml() 73 | { 74 | var schema = new XmlSchemaSet(); 75 | using (var planSchemaReader = XmlReader.Create(new StringReader(Resources.showplanxml))) 76 | { 77 | schema.Add(PlanXmlNamespace.NamespaceName, planSchemaReader); 78 | } 79 | 80 | var transform = new XslCompiledTransform(true); 81 | 82 | using (var xsltReader = XmlReader.Create(new StringReader(Resources.qpXslt))) 83 | { 84 | transform.Load(xsltReader); 85 | } 86 | 87 | var planHtml = new StringBuilder(); 88 | 89 | var settings = new XmlReaderSettings 90 | { 91 | ValidationType = ValidationType.Schema, 92 | Schemas = schema, 93 | }; 94 | 95 | using (var queryPlanReader = XmlReader.Create(new StringReader(planXml), settings)) 96 | { 97 | using (var writer = XmlWriter.Create(planHtml, transform.OutputSettings)) 98 | { 99 | transform.Transform(queryPlanReader, writer); 100 | } 101 | } 102 | return planHtml.ToString(); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/QueryPlanUserControl.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ExecutionPlanVisualizer 2 | { 3 | partial class QueryPlanUserControl 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Component Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.components = new System.ComponentModel.Container(); 32 | System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle(); 33 | this.webBrowser = new System.Windows.Forms.WebBrowser(); 34 | this.savePlanButton = new System.Windows.Forms.Button(); 35 | this.savePlanFileDialog = new System.Windows.Forms.SaveFileDialog(); 36 | this.openPlanButton = new System.Windows.Forms.Button(); 37 | this.planSavedLabel = new System.Windows.Forms.Label(); 38 | this.planLocationLinkLabel = new System.Windows.Forms.LinkLabel(); 39 | this.tabControl = new System.Windows.Forms.TabControl(); 40 | this.planTabPage = new System.Windows.Forms.TabPage(); 41 | this.indexesTabPage = new System.Windows.Forms.TabPage(); 42 | this.indexesDataGridView = new System.Windows.Forms.DataGridView(); 43 | this.progressBar = new System.Windows.Forms.ProgressBar(); 44 | this.indexLabel = new System.Windows.Forms.Label(); 45 | this.missingIndexDetailsBindingSource = new System.Windows.Forms.BindingSource(this.components); 46 | this.impactDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); 47 | this.schemaDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); 48 | this.tableDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); 49 | this.scriptDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); 50 | this.createIndexColumn = new System.Windows.Forms.DataGridViewButtonColumn(); 51 | this.tabControl.SuspendLayout(); 52 | this.planTabPage.SuspendLayout(); 53 | this.indexesTabPage.SuspendLayout(); 54 | ((System.ComponentModel.ISupportInitialize)(this.indexesDataGridView)).BeginInit(); 55 | ((System.ComponentModel.ISupportInitialize)(this.missingIndexDetailsBindingSource)).BeginInit(); 56 | this.SuspendLayout(); 57 | // 58 | // webBrowser 59 | // 60 | this.webBrowser.Dock = System.Windows.Forms.DockStyle.Fill; 61 | this.webBrowser.Location = new System.Drawing.Point(3, 3); 62 | this.webBrowser.MinimumSize = new System.Drawing.Size(20, 20); 63 | this.webBrowser.Name = "webBrowser"; 64 | this.webBrowser.Size = new System.Drawing.Size(730, 388); 65 | this.webBrowser.TabIndex = 9; 66 | // 67 | // savePlanButton 68 | // 69 | this.savePlanButton.Location = new System.Drawing.Point(245, 8); 70 | this.savePlanButton.Name = "savePlanButton"; 71 | this.savePlanButton.Size = new System.Drawing.Size(114, 23); 72 | this.savePlanButton.TabIndex = 1; 73 | this.savePlanButton.Text = "Save Plan XML"; 74 | this.savePlanButton.UseVisualStyleBackColor = true; 75 | this.savePlanButton.Click += new System.EventHandler(this.SavePlanButtonClick); 76 | // 77 | // savePlanFileDialog 78 | // 79 | this.savePlanFileDialog.Filter = "Execution Plan Files|*.sqlplan"; 80 | this.savePlanFileDialog.RestoreDirectory = true; 81 | // 82 | // openPlanButton 83 | // 84 | this.openPlanButton.AutoSize = true; 85 | this.openPlanButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; 86 | this.openPlanButton.Location = new System.Drawing.Point(7, 8); 87 | this.openPlanButton.Name = "openPlanButton"; 88 | this.openPlanButton.Size = new System.Drawing.Size(215, 23); 89 | this.openPlanButton.TabIndex = 2; 90 | this.openPlanButton.Text = "Open with Sql Server Management Studio"; 91 | this.openPlanButton.UseVisualStyleBackColor = true; 92 | this.openPlanButton.Click += new System.EventHandler(this.OpenPlanButtonClick); 93 | // 94 | // planSavedLabel 95 | // 96 | this.planSavedLabel.AutoSize = true; 97 | this.planSavedLabel.Location = new System.Drawing.Point(365, 13); 98 | this.planSavedLabel.Name = "planSavedLabel"; 99 | this.planSavedLabel.Size = new System.Drawing.Size(80, 13); 100 | this.planSavedLabel.TabIndex = 3; 101 | this.planSavedLabel.Text = "Plan Saved to: "; 102 | this.planSavedLabel.Visible = false; 103 | // 104 | // planLocationLinkLabel 105 | // 106 | this.planLocationLinkLabel.AutoSize = true; 107 | this.planLocationLinkLabel.Location = new System.Drawing.Point(438, 13); 108 | this.planLocationLinkLabel.Name = "planLocationLinkLabel"; 109 | this.planLocationLinkLabel.Size = new System.Drawing.Size(117, 13); 110 | this.planLocationLinkLabel.TabIndex = 4; 111 | this.planLocationLinkLabel.TabStop = true; 112 | this.planLocationLinkLabel.Text = "plan location goes here"; 113 | this.planLocationLinkLabel.Visible = false; 114 | this.planLocationLinkLabel.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.PlanLocationLinkLabelLinkClicked); 115 | // 116 | // tabControl 117 | // 118 | this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 119 | | System.Windows.Forms.AnchorStyles.Left) 120 | | System.Windows.Forms.AnchorStyles.Right))); 121 | this.tabControl.Controls.Add(this.planTabPage); 122 | this.tabControl.Controls.Add(this.indexesTabPage); 123 | this.tabControl.Location = new System.Drawing.Point(0, 45); 124 | this.tabControl.Name = "tabControl"; 125 | this.tabControl.SelectedIndex = 0; 126 | this.tabControl.Size = new System.Drawing.Size(744, 420); 127 | this.tabControl.TabIndex = 6; 128 | // 129 | // planTabPage 130 | // 131 | this.planTabPage.Controls.Add(this.webBrowser); 132 | this.planTabPage.Location = new System.Drawing.Point(4, 22); 133 | this.planTabPage.Name = "planTabPage"; 134 | this.planTabPage.Padding = new System.Windows.Forms.Padding(3); 135 | this.planTabPage.Size = new System.Drawing.Size(736, 394); 136 | this.planTabPage.TabIndex = 0; 137 | this.planTabPage.Text = "Query Execution Plan"; 138 | this.planTabPage.UseVisualStyleBackColor = true; 139 | // 140 | // indexesTabPage 141 | // 142 | this.indexesTabPage.Controls.Add(this.indexesDataGridView); 143 | this.indexesTabPage.Location = new System.Drawing.Point(4, 22); 144 | this.indexesTabPage.Name = "indexesTabPage"; 145 | this.indexesTabPage.Padding = new System.Windows.Forms.Padding(3); 146 | this.indexesTabPage.Size = new System.Drawing.Size(736, 394); 147 | this.indexesTabPage.TabIndex = 1; 148 | this.indexesTabPage.Text = "Missing Indexes"; 149 | this.indexesTabPage.UseVisualStyleBackColor = true; 150 | // 151 | // indexesDataGridView 152 | // 153 | this.indexesDataGridView.AllowUserToDeleteRows = false; 154 | this.indexesDataGridView.AutoGenerateColumns = false; 155 | this.indexesDataGridView.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells; 156 | this.indexesDataGridView.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.AllCells; 157 | this.indexesDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; 158 | this.indexesDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { 159 | this.impactDataGridViewTextBoxColumn, 160 | this.schemaDataGridViewTextBoxColumn, 161 | this.tableDataGridViewTextBoxColumn, 162 | this.scriptDataGridViewTextBoxColumn, 163 | this.createIndexColumn}); 164 | this.indexesDataGridView.DataSource = this.missingIndexDetailsBindingSource; 165 | this.indexesDataGridView.Dock = System.Windows.Forms.DockStyle.Fill; 166 | this.indexesDataGridView.Location = new System.Drawing.Point(3, 3); 167 | this.indexesDataGridView.Name = "indexesDataGridView"; 168 | this.indexesDataGridView.ReadOnly = true; 169 | this.indexesDataGridView.RowHeadersWidth = 4; 170 | this.indexesDataGridView.Size = new System.Drawing.Size(730, 388); 171 | this.indexesDataGridView.TabIndex = 0; 172 | this.indexesDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.IndexesDataGridViewCellContentClick); 173 | this.indexesDataGridView.DataBindingComplete += new System.Windows.Forms.DataGridViewBindingCompleteEventHandler(this.IndexesDataGridViewDataBindingComplete); 174 | // 175 | // progressBar 176 | // 177 | this.progressBar.Location = new System.Drawing.Point(577, 8); 178 | this.progressBar.Name = "progressBar"; 179 | this.progressBar.Size = new System.Drawing.Size(142, 23); 180 | this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; 181 | this.progressBar.TabIndex = 7; 182 | this.progressBar.Visible = false; 183 | // 184 | // indexLabel 185 | // 186 | this.indexLabel.AutoSize = true; 187 | this.indexLabel.Location = new System.Drawing.Point(739, 13); 188 | this.indexLabel.Name = "indexLabel"; 189 | this.indexLabel.Size = new System.Drawing.Size(74, 13); 190 | this.indexLabel.TabIndex = 8; 191 | this.indexLabel.Text = "Creating index"; 192 | this.indexLabel.Visible = false; 193 | // 194 | // missingIndexDetailsBindingSource 195 | // 196 | this.missingIndexDetailsBindingSource.DataSource = typeof(ExecutionPlanVisualizer.MissingIndexDetails); 197 | // 198 | // impactDataGridViewTextBoxColumn 199 | // 200 | this.impactDataGridViewTextBoxColumn.DataPropertyName = "Impact"; 201 | this.impactDataGridViewTextBoxColumn.FillWeight = 15F; 202 | this.impactDataGridViewTextBoxColumn.HeaderText = "Impact"; 203 | this.impactDataGridViewTextBoxColumn.Name = "impactDataGridViewTextBoxColumn"; 204 | this.impactDataGridViewTextBoxColumn.ReadOnly = true; 205 | this.impactDataGridViewTextBoxColumn.Width = 64; 206 | // 207 | // schemaDataGridViewTextBoxColumn 208 | // 209 | this.schemaDataGridViewTextBoxColumn.DataPropertyName = "Schema"; 210 | this.schemaDataGridViewTextBoxColumn.FillWeight = 15F; 211 | this.schemaDataGridViewTextBoxColumn.HeaderText = "Schema"; 212 | this.schemaDataGridViewTextBoxColumn.Name = "schemaDataGridViewTextBoxColumn"; 213 | this.schemaDataGridViewTextBoxColumn.ReadOnly = true; 214 | this.schemaDataGridViewTextBoxColumn.Width = 71; 215 | // 216 | // tableDataGridViewTextBoxColumn 217 | // 218 | this.tableDataGridViewTextBoxColumn.DataPropertyName = "Table"; 219 | this.tableDataGridViewTextBoxColumn.FillWeight = 25F; 220 | this.tableDataGridViewTextBoxColumn.HeaderText = "Table"; 221 | this.tableDataGridViewTextBoxColumn.Name = "tableDataGridViewTextBoxColumn"; 222 | this.tableDataGridViewTextBoxColumn.ReadOnly = true; 223 | this.tableDataGridViewTextBoxColumn.Width = 59; 224 | // 225 | // scriptDataGridViewTextBoxColumn 226 | // 227 | this.scriptDataGridViewTextBoxColumn.DataPropertyName = "Script"; 228 | dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True; 229 | this.scriptDataGridViewTextBoxColumn.DefaultCellStyle = dataGridViewCellStyle1; 230 | this.scriptDataGridViewTextBoxColumn.FillWeight = 50F; 231 | this.scriptDataGridViewTextBoxColumn.HeaderText = "Script"; 232 | this.scriptDataGridViewTextBoxColumn.Name = "scriptDataGridViewTextBoxColumn"; 233 | this.scriptDataGridViewTextBoxColumn.ReadOnly = true; 234 | this.scriptDataGridViewTextBoxColumn.Width = 59; 235 | // 236 | // createIndexColumn 237 | // 238 | this.createIndexColumn.FillWeight = 20F; 239 | this.createIndexColumn.HeaderText = ""; 240 | this.createIndexColumn.MinimumWidth = 100; 241 | this.createIndexColumn.Name = "createIndexColumn"; 242 | this.createIndexColumn.ReadOnly = true; 243 | this.createIndexColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True; 244 | this.createIndexColumn.Text = "Create Index"; 245 | this.createIndexColumn.UseColumnTextForButtonValue = true; 246 | // 247 | // QueryPlanUserControl 248 | // 249 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 250 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 251 | this.Controls.Add(this.indexLabel); 252 | this.Controls.Add(this.progressBar); 253 | this.Controls.Add(this.tabControl); 254 | this.Controls.Add(this.planLocationLinkLabel); 255 | this.Controls.Add(this.planSavedLabel); 256 | this.Controls.Add(this.openPlanButton); 257 | this.Controls.Add(this.savePlanButton); 258 | this.Name = "QueryPlanUserControl"; 259 | this.Size = new System.Drawing.Size(863, 529); 260 | this.tabControl.ResumeLayout(false); 261 | this.planTabPage.ResumeLayout(false); 262 | this.indexesTabPage.ResumeLayout(false); 263 | ((System.ComponentModel.ISupportInitialize)(this.indexesDataGridView)).EndInit(); 264 | ((System.ComponentModel.ISupportInitialize)(this.missingIndexDetailsBindingSource)).EndInit(); 265 | this.ResumeLayout(false); 266 | this.PerformLayout(); 267 | 268 | } 269 | 270 | #endregion 271 | 272 | private System.Windows.Forms.WebBrowser webBrowser; 273 | private System.Windows.Forms.Button savePlanButton; 274 | private System.Windows.Forms.SaveFileDialog savePlanFileDialog; 275 | private System.Windows.Forms.Button openPlanButton; 276 | private System.Windows.Forms.Label planSavedLabel; 277 | private System.Windows.Forms.LinkLabel planLocationLinkLabel; 278 | private System.Windows.Forms.TabControl tabControl; 279 | private System.Windows.Forms.TabPage planTabPage; 280 | private System.Windows.Forms.TabPage indexesTabPage; 281 | private System.Windows.Forms.DataGridView indexesDataGridView; 282 | private System.Windows.Forms.BindingSource missingIndexDetailsBindingSource; 283 | private System.Windows.Forms.ProgressBar progressBar; 284 | private System.Windows.Forms.Label indexLabel; 285 | private System.Windows.Forms.DataGridViewTextBoxColumn impactDataGridViewTextBoxColumn; 286 | private System.Windows.Forms.DataGridViewTextBoxColumn schemaDataGridViewTextBoxColumn; 287 | private System.Windows.Forms.DataGridViewTextBoxColumn tableDataGridViewTextBoxColumn; 288 | private System.Windows.Forms.DataGridViewTextBoxColumn scriptDataGridViewTextBoxColumn; 289 | private System.Windows.Forms.DataGridViewButtonColumn createIndexColumn; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/QueryPlanUserControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Windows.Forms; 6 | using ExecutionPlanVisualizer.Helpers; 7 | 8 | namespace ExecutionPlanVisualizer 9 | { 10 | public partial class QueryPlanUserControl : UserControl 11 | { 12 | private string planXml; 13 | private List indexes; 14 | 15 | public QueryPlanUserControl() 16 | { 17 | InitializeComponent(); 18 | 19 | var assocQueryString = NativeMethods.AssocQueryString(NativeMethods.AssocStr.Executable, ".sqlplan"); 20 | 21 | if (string.IsNullOrEmpty(assocQueryString)) 22 | { 23 | openPlanButton.Visible = false; 24 | } 25 | else 26 | { 27 | var fileDescription = FileVersionInfo.GetVersionInfo(assocQueryString).FileDescription; 28 | openPlanButton.Text = $"Open with {fileDescription}"; 29 | } 30 | } 31 | 32 | public string PlanHtml { get; set; } 33 | 34 | public string PlanXml { get; set; } 35 | 36 | public List Indexes { get; set; } = new List(); 37 | 38 | internal DatabaseHelper DatabaseHelper { get; set; } 39 | 40 | 41 | private void SavePlanButtonClick(object sender, EventArgs e) 42 | { 43 | if (savePlanFileDialog.ShowDialog() == DialogResult.OK) 44 | { 45 | File.WriteAllText(savePlanFileDialog.FileName, planXml); 46 | 47 | planLocationLinkLabel.Text = savePlanFileDialog.FileName; 48 | planSavedLabel.Visible = planLocationLinkLabel.Visible = true; 49 | } 50 | } 51 | 52 | private void OpenPlanButtonClick(object sender, EventArgs e) 53 | { 54 | var tempFile = Path.ChangeExtension(Path.GetTempFileName(), "sqlplan"); 55 | File.WriteAllText(tempFile, planXml); 56 | 57 | try 58 | { 59 | Process.Start(tempFile); 60 | } 61 | catch (Exception exception) 62 | { 63 | MessageBox.Show($"Cannot open execution plan. {exception.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 64 | } 65 | } 66 | 67 | private void PlanLocationLinkLabelLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 68 | { 69 | Process.Start("explorer.exe", $"/select,\"{planLocationLinkLabel.Text}\""); 70 | } 71 | 72 | private void IndexesDataGridViewDataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e) 73 | { 74 | //http://stackoverflow.com/a/10049887/239438 75 | for (int i = 0; i < indexesDataGridView.Columns.Count - 1; i++) 76 | { 77 | indexesDataGridView.Columns[i].AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells; 78 | } 79 | 80 | indexesDataGridView.Columns[indexesDataGridView.Columns.Count - 2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; 81 | 82 | for (int i = 0; i < indexesDataGridView.Columns.Count; i++) 83 | { 84 | int width = indexesDataGridView.Columns[i].Width; 85 | indexesDataGridView.Columns[i].AutoSizeMode = DataGridViewAutoSizeColumnMode.None; 86 | indexesDataGridView.Columns[i].Width = width; 87 | } 88 | } 89 | 90 | private async void IndexesDataGridViewCellContentClick(object sender, DataGridViewCellEventArgs e) 91 | { 92 | //http://stackoverflow.com/a/13687844/239438 93 | if (!(indexesDataGridView.Columns[e.ColumnIndex] is DataGridViewButtonColumn) || e.RowIndex < 0) 94 | { 95 | return; 96 | } 97 | 98 | if (MessageBox.Show("Do you really want to create this index?", "Confirm", MessageBoxButtons.YesNo, 99 | MessageBoxIcon.Warning) != DialogResult.Yes) 100 | { 101 | return; 102 | } 103 | 104 | var script = indexes[e.RowIndex].Script; 105 | 106 | try 107 | { 108 | indexesDataGridView.Enabled = false; 109 | progressBar.Visible = indexLabel.Visible = true; 110 | 111 | await DatabaseHelper.CreateIndexAsync(script); 112 | 113 | IndexCreated?.Invoke(sender, e); 114 | } 115 | catch (Exception exception) 116 | { 117 | MessageBox.Show($"Cannot create index. {exception.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 118 | } 119 | 120 | indexesDataGridView.Enabled = true; 121 | progressBar.Visible = indexLabel.Visible = false; 122 | } 123 | 124 | public event EventHandler IndexCreated; 125 | 126 | public void DisplayExecutionPlanDetails(string planXml, string planHtml, List indexes) 127 | { 128 | this.planXml = planXml; 129 | this.indexes = indexes; 130 | 131 | webBrowser.DocumentText = planHtml; 132 | 133 | if (this.indexes.Count > 0 && tabControl.TabPages.Count == 1) 134 | { 135 | tabControl.TabPages.Add(indexesTabPage); 136 | } 137 | 138 | if (this.indexes.Count == 0 && tabControl.TabPages.Count > 1) 139 | { 140 | tabControl.TabPages.Remove(indexesTabPage); 141 | } 142 | 143 | indexesTabPage.Text = $"{this.indexes.Count} Missing Index{(this.indexes.Count > 1 ? "es" : "")}"; 144 | 145 | indexesDataGridView.Columns[indexesDataGridView.ColumnCount - 1].Visible = DatabaseHelper != null; 146 | 147 | indexesDataGridView.DataSource = this.indexes; 148 | indexesDataGridView.ResetBindings(); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/QueryPlanUserControl.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 17, 17 122 | 123 | 124 | True 125 | 126 | 127 | 170, 17 128 | 129 | 130 | True 131 | 132 | 133 | 170, 17 134 | 135 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/QueryPlanVisualizer.LinqPad5.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DF42BBC7-9733-4FD3-9758-13A1F657CFBB} 8 | Library 9 | Properties 10 | ExecutionPlanVisualizer 11 | LINQPad.QueryPlanVisualizer 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll 36 | True 37 | 38 | 39 | ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll 40 | True 41 | 42 | 43 | ..\packages\LINQPad.4.51.3\lib\net40\LINQPad.exe 44 | True 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | True 69 | True 70 | Resources.resx 71 | 72 | 73 | 74 | UserControl 75 | 76 | 77 | QueryPlanUserControl.cs 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Designer 87 | 88 | 89 | 90 | 91 | ResXFileCodeGenerator 92 | Resources.Designer.cs 93 | Designer 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | QueryPlanUserControl.cs 114 | 115 | 116 | 117 | 118 | 119 | 120 | 127 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/QueryPlanVisualizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SqlClient; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Windows.Forms; 8 | using ExecutionPlanVisualizer.Helpers; 9 | using ExecutionPlanVisualizer.Properties; 10 | using LINQPad; 11 | 12 | namespace ExecutionPlanVisualizer 13 | { 14 | public static class QueryPlanVisualizer 15 | { 16 | private const string ExecutionPlanPanelTitle = "Query Execution Plan"; 17 | private static bool shouldExtract = true; 18 | 19 | public static IQueryable DumpPlan(this IQueryable queryable, bool dumpData = false) 20 | { 21 | DumpPlanInternal(queryable, dumpData, true); 22 | 23 | return queryable; 24 | } 25 | 26 | public static void DumpPlanXml(string planXml) 27 | { 28 | try 29 | { 30 | var control = new QueryPlanUserControl(); 31 | 32 | PanelManager.DisplayControl(control, ExecutionPlanPanelTitle); 33 | 34 | ProcessQueryPlan(planXml, control); 35 | } 36 | catch (Exception exception) 37 | { 38 | ShowError(exception.ToString()); 39 | } 40 | } 41 | 42 | private static void DumpPlanInternal(IQueryable queryable, bool dumpData, bool addNewPanel) 43 | { 44 | if (Util.CurrentDataContext != null && !(Util.CurrentDataContext.Connection is SqlConnection)) 45 | { 46 | ShowError("Query Plan Visualizer supports only Sql Server"); 47 | return; 48 | } 49 | 50 | var databaseHelper = DatabaseHelper.Create(Util.CurrentDataContext, queryable); 51 | 52 | if (dumpData) 53 | { 54 | queryable.Dump(); 55 | } 56 | 57 | try 58 | { 59 | var planXml = databaseHelper.GetSqlServerQueryExecutionPlan(queryable); 60 | 61 | var control = PanelManager.GetOutputPanel(ExecutionPlanPanelTitle)?.GetControl() as QueryPlanUserControl; 62 | 63 | if (control == null || addNewPanel) 64 | { 65 | control = new QueryPlanUserControl 66 | { 67 | DatabaseHelper = databaseHelper 68 | }; 69 | 70 | if (queryable != null) 71 | { 72 | control.IndexCreated += (sender, args) => 73 | { 74 | if (MessageBox.Show("Index created. Refresh query plan?", "", MessageBoxButtons.YesNo, 75 | MessageBoxIcon.Question) == DialogResult.Yes) 76 | { 77 | DumpPlanInternal(queryable, false, false); 78 | } 79 | }; 80 | } 81 | 82 | PanelManager.DisplayControl(control, ExecutionPlanPanelTitle); 83 | } 84 | 85 | ProcessQueryPlan(planXml, control); 86 | } 87 | catch (Exception exception) 88 | { 89 | ShowError(exception.ToString()); 90 | } 91 | } 92 | 93 | private static void ProcessQueryPlan(string planXml, QueryPlanUserControl control) 94 | { 95 | if (string.IsNullOrEmpty(planXml)) 96 | { 97 | ShowError("Cannot retrieve query plan"); 98 | return; 99 | } 100 | 101 | var queryPlanProcessor = new QueryPlanProcessor(planXml); 102 | 103 | var indexes = queryPlanProcessor.GetMissingIndexes(); 104 | var planHtml = queryPlanProcessor.ConvertPlanToHtml(); 105 | 106 | var files = ExtractFiles(); 107 | files.Add(planHtml); 108 | 109 | var html = string.Format(Resources.template, files.ToArray()); 110 | 111 | control.DisplayExecutionPlanDetails(planXml, html, indexes); 112 | } 113 | 114 | private static void ShowError(string text) 115 | { 116 | var control = new Label {Text = text}; 117 | PanelManager.DisplayControl(control, ExecutionPlanPanelTitle); 118 | } 119 | 120 | private static List ExtractFiles() 121 | { 122 | var folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPadQueryVisualizer"); 123 | 124 | if (!Directory.Exists(folder)) 125 | { 126 | shouldExtract = true; 127 | Directory.CreateDirectory(folder); 128 | } 129 | 130 | var qpJavascript = Path.Combine(folder, "qp.js"); 131 | var qpStyleSheet = Path.Combine(folder, "qp.css"); 132 | var jquery = Path.Combine(folder, "jquery.js"); 133 | var icons = Path.Combine(folder, "qp_icons.png"); 134 | 135 | if (shouldExtract) 136 | { 137 | Resources.qp_icons.Save(icons); 138 | 139 | File.WriteAllText(qpJavascript, Resources.qpJavascript); 140 | File.WriteAllText(qpStyleSheet, Resources.qpStyleSheet); 141 | File.WriteAllText(jquery, Resources.jquery); 142 | 143 | shouldExtract = false; 144 | } 145 | 146 | return new List { qpStyleSheet, qpJavascript, jquery }; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Resources/qp.css: -------------------------------------------------------------------------------- 1 | div.qp-node { 2 | background-color: #FFFFCC; 3 | margin: 2px; 4 | padding: 2px; 5 | border: 1px solid black; 6 | font-size: 11px; 7 | line-height: normal; 8 | } 9 | 10 | .qp-node>div { 11 | font-family: Monospace; 12 | text-align: center; 13 | } 14 | 15 | div[class|='qp-icon'] { 16 | height: 32px; 17 | width: 32px; 18 | margin-left: auto; 19 | margin-right: auto; 20 | background-repeat: no-repeat; 21 | } 22 | 23 | .qp-tt { 24 | top: 4em; 25 | left: 2em; 26 | border: 1px solid black; 27 | background-color: #FFFFEE; 28 | padding: 2px; 29 | } 30 | 31 | .qp-tt div, 32 | .qp-tt table { 33 | font-family: Sans-Serif; 34 | text-align: left; 35 | } 36 | 37 | .qp-tt table { 38 | border-width: 0px; 39 | border-spacing: 0px; 40 | margin-top: 10px; 41 | margin-bottom: 10px; 42 | width: 100%; 43 | } 44 | 45 | .qp-tt td, 46 | .qp-tt th { 47 | font-size: 11px; 48 | border-bottom: solid 1px Black; 49 | padding: 1px; 50 | } 51 | 52 | .qp-tt td { 53 | text-align: right; 54 | padding-left: 10px; 55 | } 56 | 57 | .qp-tt th { 58 | text-align: left; 59 | } 60 | 61 | .qp-bold, 62 | .qp-tt-header { 63 | font-weight: bold; 64 | } 65 | 66 | .qp-tt-header { 67 | text-align: center; 68 | } 69 | 70 | /* Icons */ 71 | .qp-icon-Catchall{background: url('qp_icons.png') -96px -0px } 72 | .qp-icon-ArithmeticExpression{background: url('qp_icons.png') -0px -0px } 73 | .qp-icon-Assert{background: url('qp_icons.png') -32px -0px } 74 | .qp-icon-Assign{background: url('qp_icons.png') -64px -0px } 75 | .qp-icon-Bitmap{background: url('qp_icons.png') -96px -0px } 76 | .qp-icon-BookmarkLookup{background: url('qp_icons.png') -128px -0px } 77 | .qp-icon-ClusteredIndexDelete{background: url('qp_icons.png') -160px -0px } 78 | .qp-icon-ClusteredIndexInsert{background: url('qp_icons.png') -192px -0px } 79 | .qp-icon-ClusteredIndexScan{background: url('qp_icons.png') -224px -0px } 80 | .qp-icon-ClusteredIndexSeek{background: url('qp_icons.png') -256px -0px } 81 | .qp-icon-ClusteredIndexUpdate{background: url('qp_icons.png') -288px -0px } 82 | .qp-icon-Collapse{background: url('qp_icons.png') -0px -32px } 83 | .qp-icon-ComputeScalar{background: url('qp_icons.png') -32px -32px } 84 | .qp-icon-Concatenation{background: url('qp_icons.png') -64px -32px } 85 | .qp-icon-ConstantScan{background: url('qp_icons.png') -96px -32px } 86 | .qp-icon-Convert{background: url('qp_icons.png') -128px -32px } 87 | .qp-icon-CursorCatchall{background: url('qp_icons.png') -96px -0px } 88 | .qp-icon-Declare{background: url('qp_icons.png') -160px -32px } 89 | .qp-icon-Delete{background: url('qp_icons.png') -288px -160px } 90 | .qp-icon-DistributeStreams{background: url('qp_icons.png') -224px -32px } 91 | .qp-icon-Dynamic{background: url('qp_icons.png') -256px -32px } 92 | .qp-icon-EagerSpool{background: url('qp_icons.png') -192px -160px } 93 | .qp-icon-FetchQuery{background: url('qp_icons.png') -288px -32px } 94 | .qp-icon-Filter{background: url('qp_icons.png') -0px -64px } 95 | .qp-icon-GatherStreams{background: url('qp_icons.png') -32px -64px } 96 | .qp-icon-HashMatch{background: url('qp_icons.png') -64px -64px } 97 | .qp-icon-HashMatchRoot{background: url('qp_icons.png') -64px -64px } 98 | .qp-icon-HashMatchTeam{background: url('qp_icons.png') -64px -64px } 99 | .qp-icon-If{background: url('qp_icons.png') -96px -64px } 100 | .qp-icon-Insert{background: url('qp_icons.png') -0px -192px } 101 | .qp-icon-InsertedScan{background: url('qp_icons.png') -128px -64px } 102 | .qp-icon-Intrinsic{background: url('qp_icons.png') -160px -64px } 103 | .qp-icon-IteratorCatchall{background: url('qp_icons.png') -96px -0px } 104 | .qp-icon-Keyset{background: url('qp_icons.png') -192px -64px } 105 | .qp-icon-LanguageElementCatchall{background: url('qp_icons.png') -96px -0px } 106 | .qp-icon-LazySpool{background: url('qp_icons.png') -192px -160px } 107 | .qp-icon-LogRowScan{background: url('qp_icons.png') -224px -64px } 108 | .qp-icon-MergeInterval{background: url('qp_icons.png') -256px -64px } 109 | .qp-icon-MergeJoin{background: url('qp_icons.png') -288px -64px } 110 | .qp-icon-NestedLoops{background: url('qp_icons.png') -0px -96px } 111 | .qp-icon-NonclusteredIndexDelete{background: url('qp_icons.png') -32px -96px } 112 | .qp-icon-NonclusteredIndexInsert{background: url('qp_icons.png') -64px -96px } 113 | .qp-icon-IndexScan{background: url('qp_icons.png') -96px -96px } 114 | .qp-icon-IndexSeek{background: url('qp_icons.png') -128px -96px } 115 | .qp-icon-NonclusteredIndexSpool{background: url('qp_icons.png') -160px -96px } 116 | .qp-icon-NonclusteredIndexUpdate{background: url('qp_icons.png') -192px -96px } 117 | .qp-icon-OnlineIndexInsert{background: url('qp_icons.png') -224px -96px } 118 | .qp-icon-ParameterTableScan{background: url('qp_icons.png') -256px -96px } 119 | .qp-icon-PopulationQuery{background: url('qp_icons.png') -288px -96px } 120 | .qp-icon-RdiLookup{background: url('qp_icons.png') -0px -128px } 121 | .qp-icon-RefreshQuery{background: url('qp_icons.png') -32px -128px } 122 | .qp-icon-RemoteDelete{background: url('qp_icons.png') -64px -128px } 123 | .qp-icon-RemoteInsert{background: url('qp_icons.png') -96px -128px } 124 | .qp-icon-RemoteQuery{background: url('qp_icons.png') -128px -128px } 125 | .qp-icon-RemoteScan{background: url('qp_icons.png') -160px -128px } 126 | .qp-icon-RemoteUpdate{background: url('qp_icons.png') -192px -128px } 127 | .qp-icon-RepartitionStreams{background: url('qp_icons.png') -224px -128px } 128 | .qp-icon-Result{background: url('qp_icons.png') -256px -128px } 129 | .qp-icon-RowCountSpool{background: url('qp_icons.png') -288px -128px } 130 | .qp-icon-Segment{background: url('qp_icons.png') -0px -160px } 131 | .qp-icon-Sequence{background: url('qp_icons.png') -32px -160px } 132 | .qp-icon-Sequenceproject{background: url('qp_icons.png') -64px -160px } 133 | .qp-icon-Snapshot{background: url('qp_icons.png') -96px -160px } 134 | .qp-icon-Sort{background: url('qp_icons.png') -128px -160px } 135 | .qp-icon-Split{background: url('qp_icons.png') -160px -160px } 136 | .qp-icon-Spool{background: url('qp_icons.png') -192px -160px } 137 | .qp-icon-Statement{background: url('qp_icons.png') -256px -128px } 138 | .qp-icon-StreamAggregate{background: url('qp_icons.png') -224px -160px } 139 | .qp-icon-Switch{background: url('qp_icons.png') -256px -160px } 140 | .qp-icon-TableDelete{background: url('qp_icons.png') -288px -160px } 141 | .qp-icon-TableInsert{background: url('qp_icons.png') -0px -192px } 142 | .qp-icon-TableScan{background: url('qp_icons.png') -32px -192px } 143 | .qp-icon-TableSpool{background: url('qp_icons.png') -64px -192px } 144 | .qp-icon-TableUpdate{background: url('qp_icons.png') -96px -192px } 145 | .qp-icon-TableValuedFunction{background: url('qp_icons.png') -128px -192px } 146 | .qp-icon-Top{background: url('qp_icons.png') -160px -192px } 147 | .qp-icon-Udx{background: url('qp_icons.png') -192px -192px } 148 | .qp-icon-Update{background: url('qp_icons.png') -96px -192px } 149 | .qp-icon-While{background: url('qp_icons.png') -224px -192px } 150 | 151 | /* Layout - can't touch this */ 152 | .qp-tt { 153 | position: absolute; 154 | visibility: hidden; 155 | z-index: 1; 156 | white-space: normal; 157 | -webkit-transition-delay: 0.5s; 158 | transition-delay: 0.5s; 159 | } 160 | 161 | div.qp-node:hover .qp-tt { 162 | visibility: visible; 163 | } 164 | 165 | .qp-tt table { 166 | white-space: nowrap; 167 | } 168 | 169 | .qp-node { 170 | position: relative; 171 | white-space: nowrap; 172 | } 173 | 174 | .qp-tr { 175 | display: table; 176 | } 177 | 178 | .qp-tr>div { 179 | display: table-cell; 180 | padding-left: 15px; 181 | } 182 | 183 | .qp-root { 184 | display: table; 185 | position: relative; 186 | } 187 | 188 | .qp-root canvas { 189 | position: absolute; 190 | width: 100%; 191 | height: 100%; 192 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Resources/qp_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/src/QueryPlanVisualizer.LinqPad5/Resources/qp_icons.png -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/Resources/template.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {3} 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/XmlUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace ExecutionPlanVisualizer 4 | { 5 | static class XmlUtils 6 | { 7 | public static string AttributeValue(this XElement element, string attribute) 8 | { 9 | return element.Attribute(attribute).Value; 10 | } 11 | 12 | public static XName WithName(this XNamespace @namespace, string name) 13 | { 14 | return @namespace + name; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad5/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/DatabaseProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Xml.Linq; 8 | using ExecutionPlanVisualizer.Helpers; 9 | 10 | namespace ExecutionPlanVisualizer 11 | { 12 | abstract class DatabaseProvider 13 | { 14 | protected DbCommand Command { get; private set; } 15 | 16 | public abstract string PlanExtension { get; } 17 | public abstract string PlanSaveDialogFilter { get; } 18 | 19 | internal void Initialize(DbCommand command) 20 | { 21 | Command = command; 22 | } 23 | 24 | public string ExtractPlan() 25 | { 26 | try 27 | { 28 | if (Command.Connection.State != ConnectionState.Open) 29 | { 30 | Command.Connection.Open(); 31 | } 32 | 33 | return ExtractPlanInternal(Command); 34 | } 35 | finally 36 | { 37 | Command.Connection.Close(); 38 | } 39 | } 40 | 41 | protected abstract string ExtractPlanInternal(DbCommand command); 42 | 43 | public virtual List GetMissingIndexes(string rawPlan) 44 | { 45 | return new List(); 46 | } 47 | 48 | public virtual Task CreateIndexAsync(string script) 49 | { 50 | throw new NotImplementedException(); 51 | } 52 | } 53 | 54 | class PostgresDatabaseProvider : DatabaseProvider 55 | { 56 | public override string PlanExtension { get; } = "txt"; 57 | public override string PlanSaveDialogFilter { get; } = "Text Files|*.txt"; 58 | 59 | protected override string ExtractPlanInternal(DbCommand command) 60 | { 61 | command.CommandText = "EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) " + command.CommandText; 62 | 63 | using var reader = command.ExecuteReader(); 64 | var plan = string.Join(Environment.NewLine, reader.Cast().Select(r => r.GetString(0))); 65 | 66 | return plan; 67 | } 68 | } 69 | 70 | class SqlServerDatabaseProvider : DatabaseProvider 71 | { 72 | private static readonly XNamespace PlanXmlNamespace = "http://schemas.microsoft.com/sqlserver/2004/07/showplan"; 73 | 74 | public override string PlanExtension { get; } = "sqlplan"; 75 | public override string PlanSaveDialogFilter { get; } = "Execution Plan Files|*.sqlplan"; 76 | 77 | protected override string ExtractPlanInternal(DbCommand command) 78 | { 79 | using var setStatisticsCommand = command.Connection.CreateCommand(); 80 | setStatisticsCommand.CommandText = "SET STATISTICS XML ON"; 81 | setStatisticsCommand.ExecuteNonQuery(); 82 | 83 | using var reader = command.ExecuteReader(); 84 | while (reader.NextResult()) 85 | { 86 | if (reader.GetName(0) == "Microsoft SQL Server 2005 XML Showplan") 87 | { 88 | reader.Read(); 89 | return reader.GetString(0); 90 | } 91 | } 92 | 93 | return null; 94 | } 95 | 96 | public override List GetMissingIndexes(string rawPlan) 97 | { 98 | var document = XDocument.Parse(rawPlan); 99 | 100 | var missingIndexGroups = document.Descendants(PlanXmlNamespace.WithName("MissingIndexGroup")); 101 | 102 | var result = from missingIndexGroup in missingIndexGroups 103 | 104 | let missingIndexes = missingIndexGroup.Descendants(PlanXmlNamespace.WithName("MissingIndex")) 105 | 106 | let indexes = from missingIndex in missingIndexes 107 | let columnGroups = missingIndex.Descendants(PlanXmlNamespace.WithName("ColumnGroup")) 108 | 109 | let equalityColumns = (from columnGroup in columnGroups 110 | where columnGroup.AttributeValue("Usage") == "EQUALITY" 111 | from column in columnGroup.Descendants() 112 | select column.AttributeValue("Name")) 113 | 114 | let inequalityColumns = (from columnGroup in columnGroups 115 | where columnGroup.AttributeValue("Usage") == "INEQUALITY" 116 | from column in columnGroup.Descendants() 117 | select column.AttributeValue("Name")) 118 | 119 | let includeColumns = (from columnGroup in columnGroups 120 | where columnGroup.AttributeValue("Usage") == "INCLUDE" 121 | from column in columnGroup.Descendants() 122 | select column.AttributeValue("Name")) 123 | 124 | select new MissingIndexDetails 125 | { 126 | Impact = Convert.ToDouble(missingIndexGroup.AttributeValue("Impact")), 127 | 128 | Database = missingIndex.AttributeValue("Database"), 129 | Table = missingIndex.AttributeValue("Table"), 130 | Schema = missingIndex.AttributeValue("Schema"), 131 | 132 | EqualityColumns = new List(equalityColumns), 133 | InequalityColumns = new List(inequalityColumns), 134 | 135 | IncludeColumns = new List(includeColumns) 136 | } 137 | 138 | from index in indexes 139 | select index; 140 | 141 | return result.ToList(); 142 | } 143 | 144 | public override async Task CreateIndexAsync(string script) 145 | { 146 | try 147 | { 148 | await Command.Connection.OpenAsync(); 149 | 150 | await using var command = Command.Connection.CreateCommand(); 151 | command.CommandText = script; 152 | await command.ExecuteNonQueryAsync(); 153 | } 154 | finally 155 | { 156 | Command.Connection.Close(); 157 | } 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Helpers/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | 6 | namespace ExecutionPlanVisualizer.Helpers 7 | { 8 | static class HttpClientExtensions 9 | { 10 | public static Task PostAsJsonAsync(this HttpClient client, string requestUrl, TValue value) 11 | { 12 | var json = JsonSerializer.Serialize(value); 13 | var stringContent = new StringContent(json, Encoding.UTF8, "application/json"); 14 | return client.PostAsync(requestUrl, stringContent); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Helpers/XmlUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace ExecutionPlanVisualizer.Helpers 4 | { 5 | static class XmlUtils 6 | { 7 | public static string AttributeValue(this XElement element, string attribute) 8 | { 9 | return element.Attribute(attribute).Value; 10 | } 11 | 12 | public static XName WithName(this XNamespace @namespace, string name) 13 | { 14 | return @namespace + name; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/MissingIndexDetails.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ExecutionPlanVisualizer 6 | { 7 | public class MissingIndexDetails 8 | { 9 | private const string CreateIndexTemplate = "CREATE NONCLUSTERED INDEX [IX_{0}_{1:yyyyMMdd_HHmmss_fff}] ON {2}.{3}.{4} ({5})"; 10 | private readonly Lazy scriptGenerator; 11 | 12 | public MissingIndexDetails() 13 | { 14 | scriptGenerator = new Lazy(CreateScript); 15 | } 16 | 17 | public double Impact { get; set; } 18 | 19 | public string Database { get; set; } 20 | public string Schema { get; set; } 21 | public string Table { get; set; } 22 | 23 | public List EqualityColumns { get; set; } 24 | public List InequalityColumns { get; set; } 25 | 26 | public List IncludeColumns { get; set; } 27 | 28 | public string Script => scriptGenerator.Value; 29 | 30 | private string CreateScript() 31 | { 32 | var indexColumns = string.Join(",", EqualityColumns.Concat(InequalityColumns)); 33 | 34 | var script = string.Format(CreateIndexTemplate, Table.Trim('[', ']'), DateTime.UtcNow, Database, Schema, Table, 35 | indexColumns); 36 | 37 | if (IncludeColumns?.Count > 0) 38 | { 39 | var includeColumns = string.Join(",", IncludeColumns); 40 | script += $" INCLUDE ({includeColumns})"; 41 | } 42 | 43 | return script; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/MyLinkLabel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Runtime.InteropServices; 4 | using System.Windows.Forms; 5 | 6 | namespace ExecutionPlanVisualizer 7 | { 8 | //From https://stackoverflow.com/a/54165759/239438 9 | public class MyLinkLabel : LinkLabel 10 | { 11 | public MyLinkLabel() 12 | { 13 | this.LinkColor = Color.FromArgb(0x00, 0x66, 0xCC); 14 | this.VisitedLinkColor = Color.FromArgb(0x80, 0x00, 0x80); 15 | this.ActiveLinkColor = Color.FromArgb(0xFF, 0x00, 0x00); 16 | } 17 | 18 | const int IDC_HAND = 32649; 19 | const int WM_SETCURSOR = 0x0020; 20 | const int HTCLIENT = 1; 21 | 22 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 23 | static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); 24 | 25 | [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] 26 | public static extern IntPtr SetCursor(HandleRef hcursor); 27 | 28 | static readonly Cursor SystemHandCursor = new Cursor(LoadCursor(IntPtr.Zero, IDC_HAND)); 29 | protected override void WndProc(ref Message msg) 30 | { 31 | if (msg.Msg == WM_SETCURSOR) 32 | { 33 | WmSetCursor(ref msg); 34 | } 35 | else 36 | { 37 | base.WndProc(ref msg); 38 | } 39 | } 40 | 41 | private void WmSetCursor(ref Message m) 42 | { 43 | if (m.WParam == (IsHandleCreated ? Handle : IntPtr.Zero) && 44 | (unchecked((int)(long)m.LParam) & 0xffff) == HTCLIENT) 45 | { 46 | if (OverrideCursor != null) 47 | { 48 | if (OverrideCursor == Cursors.Hand) 49 | { 50 | SetCursor(new HandleRef(SystemHandCursor, SystemHandCursor.Handle)); 51 | } 52 | else 53 | { 54 | SetCursor(new HandleRef(OverrideCursor, OverrideCursor.Handle)); 55 | } 56 | } 57 | else 58 | { 59 | SetCursor(new HandleRef(Cursor, Cursor.Handle)); 60 | } 61 | } 62 | else 63 | { 64 | DefWndProc(ref m); 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace ExecutionPlanVisualizer 6 | { 7 | class NativeMethods 8 | { 9 | [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)] 10 | private static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, ref uint pcchOut); 11 | 12 | public static string AssocQueryString(AssocStr association, string extension) 13 | { 14 | const int S_OK = 0; 15 | const int S_FALSE = 1; 16 | 17 | uint length = 0; 18 | var result = AssocQueryString(AssocF.None, association, extension, null, null, ref length); 19 | if (result != S_FALSE) 20 | { 21 | return null; 22 | } 23 | 24 | var stringBuilder = new StringBuilder((int)length); 25 | result = AssocQueryString(AssocF.None, association, extension, null, stringBuilder, ref length); 26 | if (result != S_OK) 27 | { 28 | return null; 29 | } 30 | 31 | return stringBuilder.ToString(); 32 | } 33 | 34 | [Flags] 35 | enum AssocF : uint 36 | { 37 | None = 0, 38 | Init_NoRemapCLSID = 0x1, 39 | Init_ByExeName = 0x2, 40 | Open_ByExeName = 0x2, 41 | Init_DefaultToStar = 0x4, 42 | Init_DefaultToFolder = 0x8, 43 | NoUserSettings = 0x10, 44 | NoTruncate = 0x20, 45 | Verify = 0x40, 46 | RemapRunDll = 0x80, 47 | NoFixUps = 0x100, 48 | IgnoreBaseClass = 0x200, 49 | Init_IgnoreUnknown = 0x400, 50 | Init_FixedProgId = 0x800, 51 | IsProtocol = 0x1000, 52 | InitForFile = 0x2000, 53 | } 54 | 55 | internal enum AssocStr 56 | { 57 | Command = 1, 58 | Executable, 59 | FriendlyDocName, 60 | FriendlyAppName, 61 | NoOpen, 62 | ShellNewValue, 63 | DDECommand, 64 | DDEIfExec, 65 | DDEApplication, 66 | DDETopic, 67 | InfoTip, 68 | QuickTip, 69 | TileInfo, 70 | ContentType, 71 | DefaultIcon, 72 | ShellExtension, 73 | DropTarget, 74 | DelegateExecute, 75 | SupportedUriProtocols, 76 | Max, 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/OrmHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace ExecutionPlanVisualizer 7 | { 8 | internal abstract class OrmHelper 9 | { 10 | public PlanProcessor PlanProcessor { get; } 11 | public DatabaseProvider DatabaseProvider { get; } 12 | 13 | public static OrmHelper Create(IQueryable queryable, object dataContext) 14 | { 15 | if (dataContext is DbContext dbContext) 16 | { 17 | var efCoreHelper = new EFCoreHelper(dbContext.Database.ProviderName); 18 | efCoreHelper.Initialize(queryable); 19 | return efCoreHelper; 20 | } 21 | 22 | var queryType = queryable.GetType(); 23 | 24 | var dataQueryType = queryType.Assembly.GetType("System.Data.Linq.DataQuery`1"); 25 | var tableQueryType = queryType.Assembly.GetType("System.Data.Linq.Table`1"); 26 | 27 | var queryGenericType = queryType.GetGenericTypeDefinition(); 28 | if (queryGenericType == dataQueryType || queryGenericType == tableQueryType) 29 | { 30 | var contextField = queryType.GetField("context", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField); 31 | var context = contextField?.GetValue(queryable); 32 | 33 | if (context != null) 34 | { 35 | var linqToSqlHelper = new LinqToSqlHelper(context); 36 | linqToSqlHelper.Initialize(queryable); 37 | return linqToSqlHelper; 38 | } 39 | } 40 | 41 | return null; 42 | } 43 | 44 | protected OrmHelper((DatabaseProvider provider, PlanProcessor planConvertor) parameters) 45 | { 46 | DatabaseProvider = parameters.provider; 47 | PlanProcessor = parameters.planConvertor; 48 | } 49 | 50 | private void Initialize(IQueryable queryable) 51 | { 52 | PlanProcessor?.Initialize(GetQueryText(queryable)); 53 | DatabaseProvider?.Initialize(CreateCommand(queryable)); 54 | } 55 | 56 | protected abstract string GetQueryText(IQueryable queryable); 57 | protected abstract DbCommand CreateCommand(IQueryable queryable); 58 | } 59 | 60 | class EFCoreHelper : OrmHelper 61 | { 62 | public EFCoreHelper(string provider) : base(CreateParameters(provider)) 63 | { 64 | } 65 | 66 | public static (DatabaseProvider provider, PlanProcessor planConvertor) CreateParameters(string provider) 67 | { 68 | return provider switch 69 | { 70 | "Microsoft.EntityFrameworkCore.SqlServer" => (new SqlServerDatabaseProvider(), new SqlServerPlanProcessor()), 71 | "Npgsql.EntityFrameworkCore.PostgreSQL" => (new PostgresDatabaseProvider(), new PostgresPlanProcessor()), 72 | _ => (null, null) 73 | }; 74 | } 75 | 76 | protected override DbCommand CreateCommand(IQueryable queryable) => queryable.CreateDbCommand(); 77 | 78 | protected override string GetQueryText(IQueryable queryable) => queryable.ToQueryString(); 79 | } 80 | 81 | class LinqToSqlHelper : OrmHelper 82 | { 83 | private readonly object dataContext; 84 | 85 | public LinqToSqlHelper(object dataContext) : base((new SqlServerDatabaseProvider(), new SqlServerPlanProcessor())) 86 | { 87 | this.dataContext = dataContext; 88 | } 89 | 90 | protected override DbCommand CreateCommand(IQueryable queryable) 91 | { 92 | var getCommand = dataContext.GetType().GetMethod("GetCommand", BindingFlags.Public | BindingFlags.Instance); 93 | return getCommand.Invoke(dataContext, new object[] { queryable }) as DbCommand; 94 | } 95 | 96 | protected override string GetQueryText(IQueryable queryable) => queryable.ToString(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/PlanConvertor.cs: -------------------------------------------------------------------------------- 1 | using ExecutionPlanVisualizer.Helpers; 2 | using System; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Text.Encodings.Web; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace ExecutionPlanVisualizer 10 | { 11 | internal abstract class PlanProcessor 12 | { 13 | public string Query { get; private set; } 14 | 15 | public abstract string SharePlanWebsite { get; } 16 | protected abstract string PlanFolder { get; } 17 | 18 | protected string PlanFileFolderFullPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 19 | "LINQPadQueryVisualizer", PlanFolder); 20 | 21 | private string planFileName = $"plan {Guid.NewGuid()}.html"; 22 | 23 | protected string PlanFilePath => Path.Combine(PlanFileFolderFullPath, planFileName); 24 | 25 | public void Initialize(string query) 26 | { 27 | Query = query; 28 | } 29 | 30 | protected abstract void ExtractFiles(); 31 | public abstract string GeneratePlanHtml(string rawPlan); 32 | public abstract Task SharePlanAsync(string plan); 33 | } 34 | 35 | class PostgresPlanProcessor : PlanProcessor 36 | { 37 | static bool shouldExtract = true; 38 | public override string SharePlanWebsite => "https://explain.dalibo.com/"; 39 | protected override string PlanFolder => "Postgres"; 40 | 41 | protected override void ExtractFiles() 42 | { 43 | Directory.CreateDirectory(Path.Combine(PlanFileFolderFullPath, "js")); 44 | Directory.CreateDirectory(Path.Combine(PlanFileFolderFullPath, "css")); 45 | 46 | if (shouldExtract) 47 | { 48 | var allStylesheet = Path.Combine(PlanFileFolderFullPath, "css", "all.css"); 49 | var appStylesheet = Path.Combine(PlanFileFolderFullPath, "css", "app.css"); 50 | var bootstrapStylesheet = Path.Combine(PlanFileFolderFullPath, "css", "bootstrap.min.css"); 51 | 52 | var chunkJavascript = Path.Combine(PlanFileFolderFullPath, "js", "chunk-vendors.js"); 53 | 54 | File.WriteAllText(allStylesheet, PostgresResources.all); 55 | File.WriteAllText(appStylesheet, PostgresResources.app_css); 56 | File.WriteAllText(bootstrapStylesheet, PostgresResources.bootstrap_min); 57 | File.WriteAllText(chunkJavascript, PostgresResources.chunk_vendors); 58 | 59 | File.WriteAllText(PlanFilePath, PostgresResources.index); 60 | } 61 | 62 | shouldExtract = false; 63 | } 64 | 65 | public override string GeneratePlanHtml(string rawPlan) 66 | { 67 | ExtractFiles(); 68 | 69 | var appJavascript = Path.Combine(PlanFileFolderFullPath, "js", "app.js"); 70 | var appJs = PostgresResources.app_js.Replace("{plan}", JavaScriptEncoder.UnsafeRelaxedJsonEscaping.Encode(rawPlan).Replace("'", "\\'")) 71 | .Replace("{query}", JavaScriptEncoder.UnsafeRelaxedJsonEscaping.Encode(Query).Replace("'", "\\'")); 72 | 73 | File.WriteAllText(appJavascript, appJs); 74 | 75 | return PlanFilePath; 76 | } 77 | 78 | public override async Task SharePlanAsync(string plan) 79 | { 80 | using var client = new HttpClient(); 81 | var data = new { plan = plan, title = "Query Plan from LINQPad", query = Query }; 82 | var responseMessage = await client.PostAsJsonAsync("https://explain.dalibo.com/new.json", data); 83 | var doc = await JsonDocument.ParseAsync(await responseMessage.Content.ReadAsStreamAsync()); 84 | var queryId = doc.RootElement.GetProperty("id").GetString(); 85 | 86 | return $"https://explain.dalibo.com/plan/{queryId}"; 87 | } 88 | } 89 | 90 | class SqlServerPlanProcessor : PlanProcessor 91 | { 92 | static bool shouldExtract = true; 93 | public override string SharePlanWebsite => "https://www.brentozar.com/pastetheplan/"; 94 | protected override string PlanFolder => "SqlServer"; 95 | 96 | protected override void ExtractFiles() 97 | { 98 | Directory.CreateDirectory(PlanFileFolderFullPath); 99 | 100 | if (shouldExtract) 101 | { 102 | var icons = Path.Combine(PlanFileFolderFullPath, "qp_icons.png"); 103 | var qpJavascript = Path.Combine(PlanFileFolderFullPath, "qp.js"); 104 | var qpStyleSheet = Path.Combine(PlanFileFolderFullPath, "qp.css"); 105 | 106 | File.WriteAllText(qpJavascript, SqlServerResources.qp_min_js); 107 | File.WriteAllText(qpStyleSheet, SqlServerResources.qp_css); 108 | SqlServerResources.qp_icons.Save(icons); 109 | } 110 | 111 | shouldExtract = false; 112 | } 113 | 114 | public override string GeneratePlanHtml(string rawPlan) 115 | { 116 | ExtractFiles(); 117 | 118 | var html = string.Format(SqlServerResources.template, rawPlan); 119 | 120 | File.WriteAllText(PlanFilePath, html); 121 | 122 | return PlanFilePath; 123 | } 124 | 125 | public override async Task SharePlanAsync(string plan) 126 | { 127 | using var client = new HttpClient(); 128 | var responseMessage = await client.PostAsJsonAsync("https://jeczi7iqj8.execute-api.us-west-2.amazonaws.com/prod/", new { queryplan_xml = plan }); 129 | var doc = await JsonDocument.ParseAsync(await responseMessage.Content.ReadAsStreamAsync()); 130 | var queryId = doc.RootElement.GetProperty("id").GetString(); 131 | 132 | return $"https://www.brentozar.com/pastetheplan/?id={queryId}"; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/PostgresResources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ExecutionPlanVisualizer { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class PostgresResources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal PostgresResources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ExecutionPlanVisualizer.PostgresResources", typeof(PostgresResources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to /*! 65 | /// * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com 66 | /// * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 67 | /// */ 68 | ///.fa, 69 | ///.fas, 70 | ///.far, 71 | ///.fal, 72 | ///.fad, 73 | ///.fab { 74 | /// -moz-osx-font-smoothing: grayscale; 75 | /// -webkit-font-smoothing: antialiased; 76 | /// display: inline-block; 77 | /// font-style: normal; 78 | /// font-variant: normal; 79 | /// text-rendering: auto; 80 | /// line-height: 1; } 81 | /// 82 | ///.fa-lg { 83 | /// font-size: 1.33333em; 84 | /// line-height: 0.75em; 85 | /// vertical-align: -. [rest of string was truncated]";. 86 | /// 87 | internal static string all { 88 | get { 89 | return ResourceManager.GetString("all", resourceCulture); 90 | } 91 | } 92 | 93 | /// 94 | /// Looks up a localized string similar to #app,body,html{height:100%}. 95 | /// 96 | internal static string app_css { 97 | get { 98 | return ResourceManager.GetString("app_css", resourceCulture); 99 | } 100 | } 101 | 102 | /// 103 | /// Looks up a localized string similar to (function(t){function e(e){for(var r,i,p=e[0],l=e[1],s=e[2],c=0,f=[];c<p.length;c++)i=p[c],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&f.push(o[i][0]),o[i]=0;for(r in l)Object.prototype.hasOwnProperty.call(l,r)&&(t[r]=l[r]);a&&a(e);while(f.length)f.shift()();return u.push.apply(u,s||[]),n()}function n(){for(var t,e=0;e<u.length;e++){for(var n=u[e],r=!0,p=1;p<n.length;p++){var l=n[p];0!==o[l]&&(r=!1)}r&&(u.splice(e--,1),t=i(i.s=n[0]))}return t}var r={},o={app:0},u=[];function i(e){if(r[e])return r[e].ex [rest of string was truncated]";. 104 | /// 105 | internal static string app_js { 106 | get { 107 | return ResourceManager.GetString("app_js", resourceCulture); 108 | } 109 | } 110 | 111 | /// 112 | /// Looks up a localized string similar to /*! 113 | /// * Bootstrap v4.5.0 (https://getbootstrap.com/) 114 | /// * Copyright 2011-2020 The Bootstrap Authors 115 | /// * Copyright 2011-2020 Twitter, Inc. 116 | /// * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 117 | /// */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning [rest of string was truncated]";. 118 | /// 119 | internal static string bootstrap_min { 120 | get { 121 | return ResourceManager.GetString("bootstrap_min", resourceCulture); 122 | } 123 | } 124 | 125 | /// 126 | /// Looks up a localized string similar to (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-vendors"],{"00ee":function(t,e,n){var r=n("b622"),i=r("toStringTag"),o={};o[i]="z",t.exports="[object z]"===String(o)},"0366":function(t,e,n){var r=n("1c0b");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 0:return function(){return t.call(e)};case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,i){return t.call(e,n,r,i)}}return function(){return t.appl [rest of string was truncated]";. 127 | /// 128 | internal static string chunk_vendors { 129 | get { 130 | return ResourceManager.GetString("chunk_vendors", resourceCulture); 131 | } 132 | } 133 | 134 | /// 135 | /// Looks up a localized string similar to <!DOCTYPE html> 136 | ///<html lang=""> 137 | /// <head> 138 | /// <meta charset="utf-8"> 139 | /// <meta http-equiv="X-UA-Compatible" content="IE=edge"> 140 | /// <meta name="viewport" content="width=device-width,initial-scale=1.0"> 141 | /// <title>my-app</title> 142 | /// <link href="css/bootstrap.min.css" rel="stylesheet" /> 143 | /// <link href="css/all.css" rel="stylesheet" /> 144 | /// <link href="css/app.css" rel="stylesheet" /> 145 | /// <link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"></head> 146 | /// [rest of string was truncated]";. 147 | /// 148 | internal static string index { 149 | get { 150 | return ResourceManager.GetString("index", resourceCulture); 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/PostgresResources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\Postgres\all.css;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 123 | 124 | 125 | Resources\Postgres\app.css;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 126 | 127 | 128 | Resources\Postgres\app.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 129 | 130 | 131 | Resources\Postgres\bootstrap.min.css;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 132 | 133 | 134 | Resources\Postgres\chunk-vendors.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 135 | 136 | 137 | Resources\Postgres\index.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 138 | 139 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/QueryPlanUserControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Windows.Forms; 8 | using Microsoft.Web.WebView2.Core; 9 | 10 | namespace ExecutionPlanVisualizer 11 | { 12 | public partial class QueryPlanUserControl : UserControl 13 | { 14 | TemporaryFiles temporaryFiles = new TemporaryFiles(); 15 | 16 | private string plan; 17 | private List indexes; 18 | 19 | public QueryPlanUserControl() 20 | { 21 | InitializeComponent(); 22 | 23 | this.Disposed += (sender, args) => temporaryFiles.Dispose(); 24 | } 25 | 26 | private void QueryPlanUserControlLoad(object sender, EventArgs e) 27 | { 28 | try 29 | { 30 | SetButtonImages(); 31 | } 32 | catch 33 | { 34 | kofiLinkLabel.Visible = true; 35 | kofiButton.Visible = githubButton.Visible = false; 36 | } 37 | 38 | var assocQueryString = NativeMethods.AssocQueryString(NativeMethods.AssocStr.Executable, $".{DatabaseProvider.PlanExtension}"); 39 | 40 | if (string.IsNullOrEmpty(assocQueryString)) 41 | { 42 | openPlanButton.Visible = false; 43 | } 44 | else 45 | { 46 | var fileDescription = FileVersionInfo.GetVersionInfo(assocQueryString).FileDescription; 47 | openPlanButton.Text = $"Open with {fileDescription}"; 48 | } 49 | 50 | sharePlanButton.Text = $"Visualize and Share Plan on {PlanProcessor.SharePlanWebsite}"; 51 | } 52 | 53 | private void SetButtonImages() 54 | { 55 | var resources = new ComponentResourceManager(typeof(QueryPlanUserControl)); 56 | 57 | kofiButton.Image = (Image)resources.GetObject("kofiButton.Image"); 58 | githubButton.Image = (Image)resources.GetObject("githubButton.Image"); 59 | } 60 | 61 | internal string WebViewFolder { get; set; } 62 | internal PlanProcessor PlanProcessor { get; set; } 63 | internal DatabaseProvider DatabaseProvider { get; set; } 64 | 65 | public async void DisplayPlan(string rawPlan) 66 | { 67 | plan = rawPlan; 68 | 69 | indexes = DatabaseProvider.GetMissingIndexes(rawPlan); 70 | var planHtmlPath = PlanProcessor.GeneratePlanHtml(rawPlan); 71 | temporaryFiles.AddFile(planHtmlPath); 72 | 73 | var userDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LINQPadQueryVisualizer", "WebView2"); 74 | 75 | var env = await CoreWebView2Environment.CreateAsync(WebViewFolder, userDataFolder); 76 | await webBrowser.EnsureCoreWebView2Async(env); 77 | 78 | webBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping("query.plan", Path.GetDirectoryName(planHtmlPath), CoreWebView2HostResourceAccessKind.Allow); 79 | webBrowser.Source = new Uri($"https://query.plan/{Path.GetFileName(planHtmlPath)}"); 80 | 81 | if (indexes.Count > 0 && tabControl.TabPages.Count == 1) 82 | { 83 | tabControl.TabPages.Add(indexesTabPage); 84 | } 85 | 86 | if (indexes.Count == 0 && tabControl.TabPages.Count > 1) 87 | { 88 | tabControl.TabPages.Remove(indexesTabPage); 89 | } 90 | 91 | indexesTabPage.Text = $"{indexes.Count} Missing Index{(indexes.Count > 1 ? "es" : "")}"; 92 | 93 | indexesDataGridView.DataSource = indexes; 94 | indexesDataGridView.ResetBindings(); 95 | } 96 | 97 | 98 | private void StartProcess(string fileName) 99 | { 100 | Process.Start(new ProcessStartInfo(fileName) 101 | { 102 | UseShellExecute = true 103 | }); 104 | } 105 | 106 | private void OpenPlanButtonClick(object sender, EventArgs e) 107 | { 108 | var filename = planLocationLinkLabel.Text; 109 | if (!File.Exists(filename)) 110 | { 111 | var tempFile = Path.GetTempFileName(); 112 | temporaryFiles.AddFile(tempFile); 113 | 114 | filename = Path.ChangeExtension(tempFile, DatabaseProvider.PlanExtension); 115 | File.WriteAllText(filename, plan); 116 | temporaryFiles.AddFile(filename); 117 | } 118 | 119 | try 120 | { 121 | StartProcess(filename); 122 | } 123 | catch (Exception exception) 124 | { 125 | MessageBox.Show($"Cannot open execution plan. {exception.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 126 | } 127 | } 128 | 129 | private void SavePlanButtonClick(object sender, EventArgs e) 130 | { 131 | savePlanFileDialog.Filter = DatabaseProvider.PlanSaveDialogFilter; 132 | 133 | if (savePlanFileDialog.ShowDialog() == DialogResult.OK) 134 | { 135 | File.WriteAllText(savePlanFileDialog.FileName, plan); 136 | 137 | planLocationLinkLabel.Text = savePlanFileDialog.FileName; 138 | planSavedLabel.Visible = planLocationLinkLabel.Visible = true; 139 | } 140 | } 141 | 142 | private void PlanLocationLinkLabelLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 143 | { 144 | Process.Start("explorer.exe", $"/select,\"{planLocationLinkLabel.Text}\""); 145 | } 146 | 147 | private async void SharePlanButtonClick(object sender, EventArgs e) 148 | { 149 | if (MessageBox.Show($"Warning: Your execution plan, including its query and parameters, will be immediately sent to {PlanProcessor.SharePlanWebsite} and stored in its database for sharing. Please review your query to make sure it doesn't contain sensitive data." 150 | + $"{Environment.NewLine}{Environment.NewLine}Do you want to continue?", 151 | "Continue", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) 152 | { 153 | return; 154 | } 155 | 156 | planSharedLabel.Visible = true; 157 | copyLinkLabel.Text = "Copy"; 158 | planSharedLabel.Text = "Sharing your plan..."; 159 | copyLinkLabel.Visible = planLinkLinkLabel.Visible = false; 160 | 161 | try 162 | { 163 | planLinkLinkLabel.Text = await PlanProcessor.SharePlanAsync(plan); 164 | 165 | copyLinkLabel.Visible = planLinkLinkLabel.Visible = true; 166 | planSharedLabel.Text = "Plan Shared."; 167 | } 168 | catch (Exception exception) 169 | { 170 | copyLinkLabel.Visible = planSharedLabel.Visible = false; 171 | MessageBox.Show($"Error sharing plan: {exception.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 172 | } 173 | } 174 | 175 | private void CopyLinkLabelLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 176 | { 177 | Clipboard.SetText(planLinkLinkLabel.Text); 178 | copyLinkLabel.Text = "Copied!"; 179 | } 180 | 181 | private void PlanLinkLinkLabelLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 182 | { 183 | StartProcess(planLinkLinkLabel.Text); 184 | } 185 | 186 | private void GitHubLinkLabelLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 187 | { 188 | StartProcess("https://github.com/Giorgi/LINQPad.QueryPlanVisualizer/"); 189 | } 190 | 191 | private void KofiLinkLabelLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) 192 | { 193 | StartProcess("https://ko-fi.com/Giorgi"); 194 | } 195 | 196 | private void KofiButtonClick(object sender, EventArgs e) 197 | { 198 | StartProcess("https://ko-fi.com/Giorgi"); 199 | } 200 | 201 | private void IndexesDataGridViewDataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e) 202 | { 203 | //http://stackoverflow.com/a/10049887/239438 204 | for (int i = 0; i < indexesDataGridView.Columns.Count - 1; i++) 205 | { 206 | indexesDataGridView.Columns[i].AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells; 207 | } 208 | 209 | indexesDataGridView.Columns[indexesDataGridView.Columns.Count - 2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; 210 | 211 | for (int i = 0; i < indexesDataGridView.Columns.Count; i++) 212 | { 213 | int width = indexesDataGridView.Columns[i].Width; 214 | indexesDataGridView.Columns[i].AutoSizeMode = DataGridViewAutoSizeColumnMode.None; 215 | indexesDataGridView.Columns[i].Width = width; 216 | } 217 | } 218 | 219 | private async void IndexesDataGridViewCellContentClick(object sender, DataGridViewCellEventArgs e) 220 | { 221 | //http://stackoverflow.com/a/13687844/239438 222 | if (!(indexesDataGridView.Columns[e.ColumnIndex] is DataGridViewButtonColumn) || e.RowIndex < 0) 223 | { 224 | return; 225 | } 226 | 227 | if (MessageBox.Show("Do you really want to create this index?", "Confirm", 228 | MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) 229 | { 230 | return; 231 | } 232 | 233 | var script = indexes[e.RowIndex].Script; 234 | 235 | try 236 | { 237 | indexesDataGridView.Enabled = false; 238 | indexProgressBar.Visible = indexLabel.Visible = true; 239 | 240 | await DatabaseProvider.CreateIndexAsync(script); 241 | 242 | if (MessageBox.Show("Index created. Refresh query plan?", "", 243 | MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 244 | { 245 | var newPlan = DatabaseProvider.ExtractPlan(); 246 | DisplayPlan(newPlan); 247 | } 248 | } 249 | catch (Exception exception) 250 | { 251 | MessageBox.Show($"Cannot create index. {exception.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 252 | } 253 | 254 | indexesDataGridView.Enabled = true; 255 | indexProgressBar.Visible = indexLabel.Visible = false; 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/QueryPlanUserControl.resx: -------------------------------------------------------------------------------- 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 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | 61 | True 62 | 63 | 64 | 65 | 66 | iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAABl0RVh0U29m 67 | dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAK3SURBVFhHxZa7TxRhFMXxkWABAQzg2kDsbHmYaEI0 68 | ofB/ICFofAQ7CjQxFvwBNJQUBiobJBI7Y+mjsUMKCjEatTFqI0ZIxDieM55vudz5dpmdmcST/BL23nPP 69 | HSb7zU5bXp3q6+8F18EDsAF+gETwb9bYo6dXY+WFsCGwAvZAWHgY9HJmSDGtC8Nd4D74A2JL8sBZZnQp 70 | Np8wwP/6HYiFFoFZ+e4GjOPguwarhJnjWhMXDCMyxgKqgNkjWndQaPSA9zIGNsEjUOSiOMNZZtg6d/Ro 71 | 7b5QXDamwKR67YDH67Pq4dg9E/ZY0kNvu2YnVbcss1cXCqPOEBiWJRU+82ScBUdVqos19Q584/F5GMSy 72 | R2VJTWuuGTgnS2Exw2UG1oKhBn6bhuVKaiohZrjMAHfWaJg2RcsncFI5hcUM8FGZnmka+MiMNa8po7SY 73 | 5bIDK2z6Y0J4e1p7fDYRs5Tp92yyueOK5INmKxMz3Q6yw4Yvki3NVSZkvnU7UhpdwLbmKhMz3Y6Uhg3Q 74 | r9nSYpbLDmyzue6KgSnNlxazXHZgnc0lVwy8BseVUVjMUFZsxxINE65oWVBOYTHDZVomaOgEP03Rswpq 75 | ysstzJzWbCyTcGdnMC+axjy4BJ6b2i/wENwE/OU8lg4aocZbfQHcAvxx40yYj7Go0XR4AOyqwRfJWcB3 76 | gFeqWRh+RKN1sQYey3MY3DWg0X9CYc4YyHlwBvgfkjGNZITeRedtxJxG9oUib+FLY3qqejfgG849cBlk 77 | bn8Qen3ALorBHfHThQbfDewjc0atXIKfF2uXeZjd/AsNwyDY0gB5Ae6Aq+AuOCFrRug1uwBmDsraXDDy 78 | JeKJBj3dsmXEnvMGmNXayw0G+K2+Ab4AG9bKBXwFPLqZU5NbGO4APJZvwDeQvm7HxJ489N4GHWo1VpIk 79 | /5Gk7S8Lp6cBadxETwAAAABJRU5ErkJggg== 80 | 81 | 82 | 83 | 84 | iVBORw0KGgoAAAANSUhEUgAAALcAAAAjCAYAAADWmZvKAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAW 85 | HAAAFhwBZKP/LwAACXlJREFUeF7tnemTXFUZxsfoZ63yT6D8kBlFEPcNCgUEF1wQt5ggIVF2QUSDBiZR 86 | KxVFSxFERHFDDOkZCISQkEnChISwJCABSSCRpDuT2TP7vh/f37337T59+/TM9PSETE+dp+qpnj7rvec+ 87 | 513OTXXK0jBmQUUitai8KllTnki1LEwk+zw9S4JVqWbhlvL1R78uOn5LpOgQFevr3lleldopNJ6eJc31 88 | yZrTEkfeEQj7nMrat4nqdzsbenqWIEXP28oqzYKyhYnUMlcDT89SJiF2Wfn61DOuSk/PUqbE4tvLJHkc 89 | dFV6epYyF1Yl28tcFZ6e84CDXtye85Ve3J7zlsWJ+/nWQdM7Mj5r7BEe6hox/zjcbT6zpd45p6fnNFmc 90 | uE8mRsYnzJr97c55PU893/vQscAAfeiRuqzy91SnzAWb681HH80uPwWcu+JWrHqxcIFfur3R3Pd6t7nv 91 | UIZ3H+wyi55scrb3LIw3PttqusXLgsGxCfPJx44H5Ut3NZu2wbGgfFSM08VbG3L6vomcubgrhAVjYiL6 92 | Ixd3Hug01+5pNb/7b6fpHA4XDvSPTpiPbSzMCmw81hf1zgazX/dMq7OP5/R45sPHgmeCiH/xUrv5zSsd 93 | 5nSx4tQ1DYyZvtFx8bgdwXN8/4aw/BRx5uLG/eSgt9eYu+8y5srlxtxwnTEPrjNmaMiYHduMuelGY5Ze 94 | Zsyv1hrT0hx1yGDZrpb02F+QHT8kFkHx8/8UZr0fi8S9cl9bMNYXaxrM/f/rCcqeON6fbnfOpuPBfdh9 95 | z5WyiqiMh3Z2ZJWUZ0jZJzZml9mkDlfN5v92bZO5+umWtItmru881WyukrIPx9w5ZK6r97SYy3Y2pwUz 96 | GWlDW/pgPd8nwvvu7hbz7tg9xfnZJxqCTb5c1jx+HYQbS2RM6j8n7ew67om+uo5niXh1ro/IPYJ9kodR 97 | bq8rfy+StcB4sb5abvPcx+vNNVLPmsWfyQw5y+JG2EsWZfPaq3LLVtycY8VtccOa+v6oxphHUn1ZdVNR 98 | xX25CEnLFteGG+rxunCs8yUuFM8ZeAxt81UJZ7gq3UyENrhXdbvwoWSvGZCNd5YISctsNov1erl9yDzb 99 | MhjMB7rFE31TQqJXpFyB5bPH/f2rncFcilTviLlYNqU9tk3i3aM9I1HrMEdhXnCFhAeuPoh/q7WuYECs 100 | MIKins+m/tGoJsTOxoH0BnhY7j2O7Q395t7XuqJvGXAt9GEzvNGduU5s1j3SXq+JDfMXWWfr1s3hrpHZ 101 | OFCYZXHfcH2ukPPxxImoU4i4uBNHMgtZKwts101FFfcOWfh/v9ETMNU7ajqGxs1XtjUGbS6RT/Avseja 102 | j2sAKniEDHhA2ubJhoGgLF/ChMsG9MVC67UMy9P7++HuoIz7Ab8Wl06fHz4XrgVCwXqtEXePe28UoWEF 103 | 43MgiIOdw8FG/KvkE4z5gNyjgu/xPnBd1GZP86D5vljmn4hnYxy8C16M0yo2LteFN8AQAO6Z/ngFXRM2 104 | 1j9l7a6XcchldH5CE8pvkbHxAklpR5i5WgwGlvtpmRtoLrVaPsEmWSeu43aZG6/N+PS3r79AzrK4V93q 105 | FnKcly8RkxE+YEVc3C+1ZazchlRvVt1UVEHFgZW6ZW9b0OZkipsHjAD5TqgBDoiItM2Xo7mrj4b3tV+s 106 | HMLCUjMX/JuIFqzYeyLdT0nCDLg+u3yrhArAJW5iZURT1zfqFM0d4jmAncBzD3slzABqST8t4QPYFHlA 107 | 5RkyPsBjaRnXAbDUel/cO0ko4qXNEflsFS/2eQkftU1C1gWw0XWsGXDm4ibWysH2GreY47z3nqhDBra4 108 | 2eE2fipWwJ57Kqq4fyzCuFAeCsRVEwrgvgkHTqa4X7OE/IENYSy6uynjfQiJgG5azvjz4beSmGk/5Q8i 109 | S/+j57OFj3UELnGzBuDRPCGeJuEc49nlJIZAxyxE3FjhfMDQYCAnOWMwPysw14px5uKGOeBK77zDLWjl 110 | qtskyczEowrcGtk38TVxmaJFrGC++DYfXTE31DiehInwBBCyaD1uFxD/8l3FjVXRNsSgYFbEHVle7pG4 111 | HEtFOKC8UgTlCku+F10n4rHL/3gwjH1d4v64JLrAFp9NDSsWR/G3UkMZEkK+FyJuxAkwIPZ9wS/VNAae 112 | AZGTp8TruUfGtOcokLMsbjA2ll/gt600ps8dMrgwLCrnNMA192RUceMBECG8RFx521B4BosVo4wk5lDX 113 | cJBo0Y83o0Atop6wsOn4jsVvj8aYTctdFblhPIaeFCyW+6580e2xPigJHjE5m+KiyKvg7ruiI1SXuOGr 114 | HWGcrqEOc90kXuA8EexSMQSARFBPgzAOhBDMo6FMIeImlMFQJSU51rCGk6RfvtxhLoi+b4lCqbX7O9In 115 | L8vEy66IwscieBLEDcZlkf/8p2xhr640pn/6wiYR4YTBNe9UzBdzA04LtF11ZJmJd3mAoE4STz2fxVpp 116 | Ft8gyR0PWr/nF/d4kKTpd8YCuyxxnxeJm9MHviMmTkcAIYpuoHqJj7VPnCtfaAuEyvVw7fxNMgiw+K4+ 117 | 39jRFGw+QHLNfQNCD+rVShO6nYhexnDPiE3H+FQkbtZYyyDiB3HPQHKq14mFZmxwc2RAGI/EGfBiSN9x 118 | 2AZihixO3PqgnSBEWfdAKOy1a2SVckMRG6+LBX1OFubBIz2BxVULNhNy7Ic1tIkVJrO3z4BPrz4WvOJ/ 119 | SoTHQ+HkwT6eg4QK1O9uHgisC/+eBrjCBUhCxjx22e1iqezjOa7hLrHSiE3LsPCEGZzwcJpCgjfVyyu8 120 | GiIjVOLayA04Vou/Erd54ZbwzB9Pslms762ySewzddaeManHk10k7e3+XDthm8vwcA3LJZyIlxMGkjyz 121 | hmzo+FElZ+RsMPIZyIuhye5hmixO3GOTZQOKF/aJKcicc+aDnbTNFfLCBsFhwXn5gLXB8rARXe095xSL 122 | E7f++4JigSuezhu5N5t6omKDe9bkynNOszhx62lCsbDfEs41EjoQVvzhQFdwJJkv1vaccyxO3MSdxG+8 123 | tCA5gRxpddiUxIXkRMmBPYkFJGngJEJfeHh6ziKLE7en5xymF7fnvKUXt+e8JeJODjgqPD1LmuHvliSS 124 | e1yVnp6lzOAXp/xvBXrORwa/FXhpwrxVTLj/lVfPeUOx2tvSv9Md/D53IlXraujpWVJMJLe+6/7Dbw+E 125 | nUalWVBRnfwWlcEv1bt+wd7Tcy6yKtUk4fXm8qqjX8v8zwplZf8H2Bb2sSr+14kAAAAASUVORK5CYII= 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/QueryPlanVisualizer.LinqPad6.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | ExecutionPlanVisualizer 7 | true 8 | true 9 | true 10 | LINQPadQueryPlanVisualizer 11 | Giorgi Dalakishvili 12 | LINQPadQueryPlanVisualizer 13 | 2.1.0 14 | https://github.com/Giorgi/LINQPad.QueryPlanVisualizer/ 15 | LINQPad SQL SQLServer PostgreSQL Postgres npgsql EFCore 16 | git 17 | https://github.com/Giorgi/LINQPad.QueryPlanVisualizer/ 18 | SQL Server and PostgreSQL query execution plan visualizer for LINQPad. 19 | 20 | Features Include: 21 | 22 | • View query execution plan inside LINQPad 23 | • View missing indexes for query 24 | • Share plan to https://www.brentozar.com/pastetheplan/ or https://explain.dalibo.com/ 25 | • Create missing indexes directly from LINQPad 26 | • Open plan in SQL Server Management Studio or other default app 27 | • Save plan to disk 28 | Copyright (c) 2016 - 2021 Giorgi Dalakishvili 29 | License.md 30 | Use Microsoft Edge WebView2 to display query plan 31 | 32 | Added Support for LINQPad 6 33 | 34 | Added Support for PostgreSQL 35 | 36 | Icon.png 37 | true 38 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | True 51 | 52 | 53 | 54 | True 55 | 56 | 57 | 58 | 59 | 60 | 61 | True 62 | True 63 | PostgresResources.resx 64 | 65 | 66 | True 67 | True 68 | SqlServerResources.resx 69 | 70 | 71 | 72 | 73 | 74 | ResXFileCodeGenerator 75 | PostgresResources.Designer.cs 76 | 77 | 78 | ResXFileCodeGenerator 79 | SqlServerResources.Designer.cs 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/QueryPlanVisualizer.cs: -------------------------------------------------------------------------------- 1 | using LINQPad; 2 | using System; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Windows.Forms; 6 | using Microsoft.Web.WebView2.Core; 7 | 8 | namespace ExecutionPlanVisualizer 9 | { 10 | public static class QueryPlanVisualizer 11 | { 12 | private const string ExecutionPlanPanelTitle = "Query Execution Plan"; 13 | 14 | public static IQueryable DumpPlan(this IQueryable queryable, bool dumpData = false) 15 | { 16 | try 17 | { 18 | DumpPlanInternal(queryable, dumpData); 19 | } 20 | catch (Exception exception) 21 | { 22 | ShowError(exception.Message); 23 | } 24 | 25 | return queryable; 26 | } 27 | 28 | private static void DumpPlanInternal(IQueryable queryable, bool dumpData) 29 | { 30 | var ormHelper = OrmHelper.Create(queryable, Util.CurrentDataContext); 31 | 32 | if (ormHelper == null) 33 | { 34 | ShowError("The selected database or database driver isn't supported"); 35 | return; 36 | } 37 | 38 | if (dumpData) 39 | { 40 | queryable.Dump(); 41 | } 42 | 43 | if (ormHelper.DatabaseProvider == null) 44 | { 45 | ShowError("Selected database not supported"); 46 | return; 47 | } 48 | 49 | var rawPlan = ormHelper.DatabaseProvider.ExtractPlan(); 50 | 51 | if (string.IsNullOrEmpty(rawPlan)) 52 | { 53 | ShowError("Cannot extract query plan"); 54 | return; 55 | } 56 | 57 | string webViewFolder = null; 58 | 59 | try 60 | { 61 | CoreWebView2Environment.GetAvailableBrowserVersionString(); 62 | } 63 | catch (WebView2RuntimeNotFoundException) 64 | { 65 | var nestedType = typeof(Util).GetNestedType("BrowserEngine"); 66 | 67 | var methodInfo = nestedType?.GetMethod("GetWebView2ExecutableFolder", BindingFlags.Static | BindingFlags.Public); 68 | webViewFolder = methodInfo?.Invoke(null, null)?.ToString(); 69 | 70 | if (nestedType == null || methodInfo == null || string.IsNullOrEmpty(webViewFolder)) 71 | { 72 | ShowError("Query Plan Visualizer requires Webview2 Runtime installation but it was not found."); 73 | return; 74 | } 75 | 76 | try 77 | { 78 | var browserVersionString = CoreWebView2Environment.GetAvailableBrowserVersionString(webViewFolder); 79 | } 80 | catch (WebView2RuntimeNotFoundException) 81 | { 82 | ShowError("Query Plan Visualizer requires Webview2 Runtime installation but it was not found."); 83 | return; 84 | } 85 | } 86 | 87 | var control = new QueryPlanUserControl 88 | { 89 | WebViewFolder = webViewFolder, 90 | DatabaseProvider = ormHelper.DatabaseProvider, 91 | PlanProcessor = ormHelper.PlanProcessor 92 | }; 93 | 94 | control.DisplayPlan(rawPlan); 95 | control.Dump(ExecutionPlanPanelTitle); 96 | } 97 | 98 | private static void ShowError(string text) 99 | { 100 | var control = new Label { Text = text }; 101 | control.Dump(ExecutionPlanPanelTitle); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Resources/Postgres/app.css: -------------------------------------------------------------------------------- 1 | #app,body,html{height:100%} -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Resources/Postgres/app.js: -------------------------------------------------------------------------------- 1 | (function(t){function e(e){for(var r,i,p=e[0],l=e[1],s=e[2],c=0,f=[];c 2 | 3 | 4 | 5 | 6 | 7 | my-app 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Resources/SqlServer/qp.css: -------------------------------------------------------------------------------- 1 | div.qp-node { 2 | background-color: #FFFFCC; 3 | margin: 2px; 4 | padding: 2px; 5 | border: 1px solid black; 6 | } 7 | 8 | div.qp-node, 9 | div.qp-tt { 10 | font-size: 11px; 11 | line-height: normal; 12 | } 13 | 14 | .qp-statement-header { 15 | display: none; 16 | border-color: black; 17 | border-style: solid; 18 | border-width: 1px 0px 1px 0px; 19 | padding: 0.3em; 20 | } 21 | 22 | .qp-statement-header-row { 23 | width: 100%; 24 | height: 15.6px; 25 | } 26 | 27 | .qp-statement-header-row > div { 28 | width: 100%; 29 | position: absolute; 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | white-space: nowrap; 33 | } 34 | 35 | .missing-index { 36 | color: green; 37 | } 38 | 39 | .qp-statement-header, 40 | .qp-node>div { 41 | font-family: Monospace; 42 | } 43 | 44 | .qp-node>div { 45 | text-align: center; 46 | } 47 | 48 | div[class|='qp-icon'] { 49 | height: 32px; 50 | width: 32px; 51 | margin-left: auto; 52 | margin-right: auto; 53 | background-repeat: no-repeat; 54 | position: relative; 55 | } 56 | 57 | .qp-tt { 58 | top: 4em; 59 | left: 2em; 60 | border: 1px solid black; 61 | background-color: #FFFFEE; 62 | padding: 2px; 63 | width: 30em; 64 | } 65 | 66 | .qp-tt div, 67 | .qp-tt table { 68 | font-family: Sans-Serif; 69 | text-align: left; 70 | } 71 | 72 | .qp-tt table { 73 | border-width: 0px; 74 | border-spacing: 0px; 75 | margin-top: 10px; 76 | margin-bottom: 10px; 77 | width: 100%; 78 | } 79 | 80 | .qp-tt td, 81 | .qp-tt th { 82 | font-size: 11px; 83 | border-bottom: solid 1px Black; 84 | padding: 1px; 85 | } 86 | 87 | .qp-tt td { 88 | text-align: right; 89 | padding-left: 10px; 90 | } 91 | 92 | .qp-tt th { 93 | text-align: left; 94 | } 95 | 96 | .qp-bold, 97 | .qp-tt-header { 98 | font-weight: bold; 99 | } 100 | 101 | .qp-tt-header { 102 | text-align: center; 103 | } 104 | 105 | /* Icons */ 106 | .qp-icon-Catchall{background: url('qp_icons.png') -96px -256px } 107 | .qp-icon-ArithmeticExpression{background: url('qp_icons.png') -0px -0px } 108 | .qp-icon-Assert{background: url('qp_icons.png') -32px -0px } 109 | .qp-icon-Assign{background: url('qp_icons.png') -64px -0px } 110 | .qp-icon-Bitmap{background: url('qp_icons.png') -256px -192px } 111 | .qp-icon-BookmarkLookup{background: url('qp_icons.png') -128px -0px } 112 | .qp-icon-ClusteredIndexDelete{background: url('qp_icons.png') -160px -0px } 113 | .qp-icon-ClusteredIndexInsert{background: url('qp_icons.png') -192px -0px } 114 | .qp-icon-ClusteredIndexScan{background: url('qp_icons.png') -224px -0px } 115 | .qp-icon-ClusteredIndexSeek{background: url('qp_icons.png') -256px -0px } 116 | .qp-icon-ClusteredIndexMerge{background: url('qp_icons.png') -0px -256px } 117 | .qp-icon-KeyLookup{background: url('qp_icons.png') -256px -0px } 118 | .qp-icon-ClusteredIndexUpdate{background: url('qp_icons.png') -288px -0px } 119 | .qp-icon-Collapse{background: url('qp_icons.png') -0px -32px } 120 | .qp-icon-ComputeScalar{background: url('qp_icons.png') -32px -32px } 121 | .qp-icon-Concatenation{background: url('qp_icons.png') -64px -32px } 122 | .qp-icon-ConstantScan{background: url('qp_icons.png') -96px -32px } 123 | .qp-icon-Convert{background: url('qp_icons.png') -128px -32px } 124 | .qp-icon-CursorCatchall{background: url('qp_icons.png') -96px -0px } 125 | .qp-icon-Declare{background: url('qp_icons.png') -160px -32px } 126 | .qp-icon-Delete{background: url('qp_icons.png') -288px -160px } 127 | .qp-icon-DistributeStreams{background: url('qp_icons.png') -224px -32px } 128 | .qp-icon-Dynamic{background: url('qp_icons.png') -256px -32px } 129 | .qp-icon-EagerSpool{background: url('qp_icons.png') -192px -160px } 130 | .qp-icon-FetchQuery{background: url('qp_icons.png') -288px -32px } 131 | .qp-icon-Filter{background: url('qp_icons.png') -0px -64px } 132 | .qp-icon-GatherStreams{background: url('qp_icons.png') -32px -64px } 133 | .qp-icon-HashMatch{background: url('qp_icons.png') -64px -64px } 134 | .qp-icon-HashMatchRoot{background: url('qp_icons.png') -64px -64px } 135 | .qp-icon-HashMatchTeam{background: url('qp_icons.png') -64px -64px } 136 | .qp-icon-If{background: url('qp_icons.png') -96px -64px } 137 | .qp-icon-Insert{background: url('qp_icons.png') -0px -192px } 138 | .qp-icon-InsertedScan{background: url('qp_icons.png') -128px -64px } 139 | .qp-icon-Intrinsic{background: url('qp_icons.png') -160px -64px } 140 | .qp-icon-IteratorCatchall{background: url('qp_icons.png') -96px -0px } 141 | .qp-icon-Keyset{background: url('qp_icons.png') -192px -64px } 142 | .qp-icon-LanguageElementCatchall{background: url('qp_icons.png') -96px -0px } 143 | .qp-icon-LazySpool{background: url('qp_icons.png') -192px -160px } 144 | .qp-icon-LogRowScan{background: url('qp_icons.png') -224px -64px } 145 | .qp-icon-MergeInterval{background: url('qp_icons.png') -256px -64px } 146 | .qp-icon-MergeJoin{background: url('qp_icons.png') -288px -64px } 147 | .qp-icon-NestedLoops{background: url('qp_icons.png') -0px -96px } 148 | .qp-icon-NonclusteredIndexDelete{background: url('qp_icons.png') -32px -96px } 149 | .qp-icon-NonclusteredIndexInsert{background: url('qp_icons.png') -64px -96px } 150 | .qp-icon-IndexScan{background: url('qp_icons.png') -96px -96px } 151 | .qp-icon-IndexSeek{background: url('qp_icons.png') -128px -96px } 152 | .qp-icon-NonclusteredIndexSpool{background: url('qp_icons.png') -160px -96px } 153 | .qp-icon-NonclusteredIndexUpdate{background: url('qp_icons.png') -192px -96px } 154 | .qp-icon-OnlineIndexInsert{background: url('qp_icons.png') -224px -96px } 155 | .qp-icon-ParameterTableScan{background: url('qp_icons.png') -256px -96px } 156 | .qp-icon-PopulateQuery{background: url('qp_icons.png') -192px -224px } 157 | .qp-icon-RdiLookup{background: url('qp_icons.png') -0px -128px } 158 | .qp-icon-RefreshQuery{background: url('qp_icons.png') -32px -128px } 159 | .qp-icon-RemoteDelete{background: url('qp_icons.png') -64px -128px } 160 | .qp-icon-RemoteInsert{background: url('qp_icons.png') -96px -128px } 161 | .qp-icon-RemoteQuery{background: url('qp_icons.png') -128px -128px } 162 | .qp-icon-RemoteScan{background: url('qp_icons.png') -160px -128px } 163 | .qp-icon-RemoteUpdate{background: url('qp_icons.png') -192px -128px } 164 | .qp-icon-RepartitionStreams{background: url('qp_icons.png') -224px -128px } 165 | .qp-icon-Result{background: url('qp_icons.png') -256px -128px } 166 | .qp-icon-RowCountSpool{background: url('qp_icons.png') -288px -128px } 167 | .qp-icon-Segment{background: url('qp_icons.png') -0px -160px } 168 | .qp-icon-Sequence{background: url('qp_icons.png') -32px -160px } 169 | .qp-icon-SequenceProject{background: url('qp_icons.png') -224px -224px } 170 | .qp-icon-SnapShot{background: url('qp_icons.png') -256px -224px } 171 | .qp-icon-Sort{background: url('qp_icons.png') -128px -160px } 172 | .qp-icon-Split{background: url('qp_icons.png') -160px -160px } 173 | .qp-icon-Spool{background: url('qp_icons.png') -192px -160px } 174 | .qp-icon-Statement{background: url('qp_icons.png') -256px -128px } 175 | .qp-icon-StreamAggregate{background: url('qp_icons.png') -224px -160px } 176 | .qp-icon-Switch{background: url('qp_icons.png') -256px -160px } 177 | .qp-icon-TableDelete{background: url('qp_icons.png') -288px -160px } 178 | .qp-icon-TableInsert{background: url('qp_icons.png') -0px -192px } 179 | .qp-icon-TableScan{background: url('qp_icons.png') -32px -192px } 180 | .qp-icon-TableSpool{background: url('qp_icons.png') -64px -192px } 181 | .qp-icon-WindowSpool{background: url('qp_icons.png') -64px -192px } 182 | .qp-icon-TableUpdate{background: url('qp_icons.png') -96px -192px } 183 | .qp-icon-TableValuedFunction{background: url('qp_icons.png') -128px -192px } 184 | .qp-icon-Top{background: url('qp_icons.png') -160px -192px } 185 | .qp-icon-UDX{background: url('qp_icons.png') -192px -192px } 186 | .qp-icon-Update{background: url('qp_icons.png') -96px -192px } 187 | .qp-icon-While{background: url('qp_icons.png') -224px -192px } 188 | .qp-icon-StmtCursor{background: url('qp_icons.png') -96px -256px } 189 | .qp-icon-StmtCond{background: url('qp_icons.png') -0px -224px } 190 | .qp-icon-FastForward{background: url('qp_icons.png') -96px -0px } 191 | .qp-icon-WindowAggregate{background: url('qp_icons.png') -160px -256px } 192 | .qp-icon-AdaptiveJoin{background: url('qp_icons.png') -288px -224px } 193 | .qp-icon-IndexSpool{background: url('qp_icons.png') -160px -96px } 194 | .qp-icon-IndexInsert{background: url('qp_icons.png') -64px -96px } 195 | .qp-icon-IndexDelete{background: url('qp_icons.png') -32px -96px } 196 | .qp-icon-IndexUpdate{background: url('qp_icons.png') -192px -96px } 197 | .qp-icon-ColumnStoreIndexScan{background: url('qp_icons.png') -128px -224px } 198 | .qp-icon-ColumnStoreIndexInsert{background: url('qp_icons.png') -64px -224px } 199 | .qp-icon-ColumnStoreIndexDelete{background: url('qp_icons.png') -32px -224px } 200 | .qp-icon-ColumnStoreIndexUpdate{background: url('qp_icons.png') -160px -224px } 201 | .qp-icon-ColumnStoreIndexMerge{background: url('qp_icons.png') -96px -224px } 202 | .qp-icon-DeletedScan{background: url('qp_icons.png') -32px -256px } 203 | .qp-icon-TableMerge{background: url('qp_icons.png') -65px -256px } 204 | .qp-icon-BatchHashTableBuild{background: url('qp_icons.png') -128px -256px } 205 | 206 | .qp-iconwarn { 207 | background: url('qp_icons.png') -304px -208px; 208 | height: 16px; 209 | width: 16px; 210 | position: absolute; 211 | top: 16px; 212 | } 213 | 214 | .qp-iconpar { 215 | background: url('qp_icons.png') -304px -192px; 216 | height: 16px; 217 | width: 16px; 218 | position: absolute; 219 | top: 16px; 220 | left: 16px; 221 | } 222 | 223 | .qp-iconbatch { 224 | background: url('qp_icons.png') -288px -192px; 225 | height: 16px; 226 | width: 16px; 227 | position: absolute; 228 | top: 16px; 229 | } 230 | 231 | /* Layout - can't touch this */ 232 | .qp-tt { 233 | position: absolute; 234 | z-index: 1; 235 | white-space: normal; 236 | -webkit-transition-delay: 0.5s; 237 | transition-delay: 0.5s; 238 | } 239 | 240 | div.qp-node .qp-tt, 241 | .qp-noCssTooltip div.qp-node:hover .qp-tt { 242 | visibility: collapse; 243 | } 244 | 245 | div.qp-node:hover .qp-tt { 246 | visibility: visible; 247 | } 248 | 249 | .qp-tt table { 250 | white-space: nowrap; 251 | } 252 | 253 | .qp-node { 254 | position: relative; 255 | white-space: nowrap; 256 | display: inline-block; 257 | vertical-align: middle; 258 | } 259 | 260 | .qp-node-outer { 261 | height: 76.4px; 262 | display: table-cell; 263 | } 264 | 265 | .qp-tr { 266 | display: table; 267 | } 268 | 269 | .qp-tr>div { 270 | display: table-cell; 271 | padding-left: 20px; 272 | } 273 | 274 | .qp-root { 275 | margin-left: -25px; 276 | position: relative; 277 | /*display: inline-block;*/ 278 | } 279 | 280 | .qp-root svg { 281 | position: absolute; 282 | width: 100%; 283 | height: 100%; 284 | top: 0; 285 | left: 0; 286 | pointer-events: none; 287 | } 288 | 289 | .qp-root polyline { 290 | pointer-events: auto; 291 | } 292 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Resources/SqlServer/qp_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Giorgi/LINQPad.QueryPlanVisualizer/093f853de63260b04d8a724c56bdf5fee82e017e/src/QueryPlanVisualizer.LinqPad6/Resources/SqlServer/qp_icons.png -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/Resources/SqlServer/template.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 17 | 18 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/SqlServerResources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ExecutionPlanVisualizer { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class SqlServerResources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal SqlServerResources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ExecutionPlanVisualizer.SqlServerResources", typeof(SqlServerResources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to div.qp-node { 65 | /// background-color: #FFFFCC; 66 | /// margin: 2px; 67 | /// padding: 2px; 68 | /// border: 1px solid black; 69 | ///} 70 | /// 71 | ///div.qp-node, 72 | ///div.qp-tt { 73 | /// font-size: 11px; 74 | /// line-height: normal; 75 | ///} 76 | /// 77 | ///.qp-statement-header { 78 | /// display: none; 79 | /// border-color: black; 80 | /// border-style: solid; 81 | /// border-width: 1px 0px 1px 0px; 82 | /// padding: 0.3em; 83 | ///} 84 | /// 85 | ///.qp-statement-header-row { 86 | /// width: 100%; 87 | /// height: 15.6px; 88 | ///} 89 | /// 90 | ///.qp-statement-header-row > div { 91 | /// width: 100%; 92 | /// position: absolute; 93 | /// ov [rest of string was truncated]";. 94 | /// 95 | internal static string qp_css { 96 | get { 97 | return ResourceManager.GetString("qp-css", resourceCulture); 98 | } 99 | } 100 | 101 | /// 102 | /// Looks up a localized resource of type System.Drawing.Bitmap. 103 | /// 104 | internal static System.Drawing.Bitmap qp_icons { 105 | get { 106 | object obj = ResourceManager.GetObject("qp_icons", resourceCulture); 107 | return ((System.Drawing.Bitmap)(obj)); 108 | } 109 | } 110 | 111 | /// 112 | /// Looks up a localized string similar to !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.QP=e():t.QP=e()}(window,function(){return function(t){var e={};function n(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symb [rest of string was truncated]";. 113 | /// 114 | internal static string qp_min_js { 115 | get { 116 | return ResourceManager.GetString("qp-min-js", resourceCulture); 117 | } 118 | } 119 | 120 | /// 121 | /// Looks up a localized string similar to <!DOCTYPE HTML> 122 | ///<html> 123 | ///<head> 124 | /// <meta charset="UTF-8"> 125 | /// <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> 126 | /// <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 127 | /// <link type="text/css" media="all" rel="stylesheet" href="qp.css" /> 128 | /// <script type="text/javascript" src="qp.js"></script> 129 | ///</head> 130 | /// <body> 131 | /// <div id="container"> 132 | /// 133 | /// </div> 134 | /// <script> 135 | /// QP.showPlan(document.getElementById("container"), '{0}'); 136 | /// </script> 137 | /// </body> /// [rest of string was truncated]";. 138 | /// 139 | internal static string template { 140 | get { 141 | return ResourceManager.GetString("template", resourceCulture); 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/SqlServerResources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\SqlServer\qp.min.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 123 | 124 | 125 | Resources\SqlServer\template.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 126 | 127 | 128 | Resources\SqlServer\qp.css;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 129 | 130 | 131 | Resources\SqlServer\qp_icons.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 132 | 133 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.LinqPad6/TemporaryFiles.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace ExecutionPlanVisualizer 6 | { 7 | 8 | public class TemporaryFiles : IDisposable 9 | { 10 | private List filesToDelete; 11 | private List foldersToDelete; 12 | 13 | 14 | public TemporaryFiles() 15 | { 16 | filesToDelete = new List(); 17 | foldersToDelete = new List(); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | Dispose(disposing: true); 23 | GC.SuppressFinalize(this); 24 | } 25 | 26 | protected virtual void Dispose(bool disposing) 27 | { 28 | DeleteFiles(); 29 | } 30 | 31 | ~TemporaryFiles() 32 | { 33 | Dispose(disposing: false); 34 | } 35 | 36 | public void AddFile(string fileName) 37 | { 38 | if (String.IsNullOrWhiteSpace(fileName)) 39 | throw new ArgumentNullException(nameof(fileName)); 40 | 41 | filesToDelete.Add(fileName); 42 | } 43 | 44 | 45 | public void AddFolder(string folderName) 46 | { 47 | if (String.IsNullOrWhiteSpace(folderName)) 48 | throw new ArgumentNullException(nameof(folderName)); 49 | 50 | foldersToDelete.Add(folderName); 51 | } 52 | 53 | public string GetTemporaryFileName() 54 | { 55 | var tempFile = Path.GetTempFileName(); 56 | AddFile(tempFile); 57 | return tempFile; 58 | } 59 | 60 | public string CreateTemporaryFolder(string folderName = null) 61 | { 62 | if (String.IsNullOrWhiteSpace(folderName)) 63 | folderName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 64 | 65 | Directory.CreateDirectory(folderName); 66 | foldersToDelete.Add(folderName); 67 | 68 | return folderName; 69 | } 70 | 71 | 72 | private void Try(Action action) 73 | { 74 | try { action(); } catch { } 75 | } 76 | 77 | 78 | public void DeleteFiles() 79 | { 80 | filesToDelete.ForEach(fileName => Try(() => File.Delete(fileName))); 81 | filesToDelete.Clear(); 82 | 83 | foldersToDelete.ForEach(folder => Try(() => Directory.Delete(folder))); 84 | foldersToDelete.Clear(); 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/QueryPlanVisualizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30711.63 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryPlanVisualizer.LinqPad5", "QueryPlanVisualizer.LinqPad5\QueryPlanVisualizer.LinqPad5.csproj", "{DF42BBC7-9733-4FD3-9758-13A1F657CFBB}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryPlanVisualizer.LinqPad6", "QueryPlanVisualizer.LinqPad6\QueryPlanVisualizer.LinqPad6.csproj", "{2758E806-1292-4328-8A36-2143EF2533D5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {DF42BBC7-9733-4FD3-9758-13A1F657CFBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {DF42BBC7-9733-4FD3-9758-13A1F657CFBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {DF42BBC7-9733-4FD3-9758-13A1F657CFBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {DF42BBC7-9733-4FD3-9758-13A1F657CFBB}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2758E806-1292-4328-8A36-2143EF2533D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2758E806-1292-4328-8A36-2143EF2533D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2758E806-1292-4328-8A36-2143EF2533D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2758E806-1292-4328-8A36-2143EF2533D5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {277FEC5F-DC8B-4542-A490-F44EFBB8B240} 30 | EndGlobalSection 31 | EndGlobal 32 | --------------------------------------------------------------------------------