* Return the estimated execution plan for a query using SHOWPLAN_XML. * * SHOWPLAN_XML compiles the statement and returns its plan without executing * it, but it has two constraints: `SET SHOWPLAN_XML ON` must be the only * statement in its batch, and the setting is session scoped. The s
(innerQuery: string)
| 816 | * with SHOWPLAN enabled (which would return a plan instead of its results). |
| 817 | */ |
| 818 | private async explainQuery(innerQuery: string): Promise<SQLResult> { |
| 819 | // Validate against comment/string-stripped SQL so comment-only input counts |
| 820 | // as empty and a SET SHOWPLAN can't hide behind comments. |
| 821 | const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim(); |
| 822 | if (!cleaned) { |
| 823 | throw new Error("EXPLAIN requires a statement to analyze"); |
| 824 | } |
| 825 | |
| 826 | // Defense in depth: the SET SHOWPLAN session toggle is what makes EXPLAIN |
| 827 | // non-executing, so the explained statement must not disable it. SQL Server |
| 828 | // already rejects `SET SHOWPLAN_* OFF` alongside other statements in a |
| 829 | // batch, but enforcing it here keeps the read-only guarantee self-contained. |
| 830 | if (/\bset\s+showplan/i.test(cleaned)) { |
| 831 | throw new Error("EXPLAIN does not support SET SHOWPLAN statements"); |
| 832 | } |
| 833 | |
| 834 | if (!this.config) { |
| 835 | throw new Error("Not connected to SQL Server database"); |
| 836 | } |
| 837 | |
| 838 | const explainPool = new sql.ConnectionPool({ |
| 839 | ...this.config, |
| 840 | pool: { ...this.config.pool, max: 1, min: 1 }, |
| 841 | }); |
| 842 | |
| 843 | try { |
| 844 | await explainPool.connect(); |
| 845 | // max:1 + sequential awaits guarantee both batches hit the same session. |
| 846 | await explainPool.request().batch("SET SHOWPLAN_XML ON"); |
| 847 | const planResult = await explainPool.request().batch(innerQuery); |
| 848 | |
| 849 | // The plan is returned as the single column of the first row. |
| 850 | const planRow = planResult.recordset?.[0]; |
| 851 | const planXml = planRow ? Object.values(planRow)[0] : null; |
| 852 | return { |
| 853 | rows: planXml != null ? [{ plan: planXml }] : [], |
| 854 | rowCount: planXml != null ? 1 : 0, |
| 855 | }; |
| 856 | } catch (error) { |
| 857 | throw new Error(`Failed to explain query: ${(error as Error).message}`); |
| 858 | } finally { |
| 859 | await explainPool.close(); |
| 860 | } |
| 861 | } |
| 862 | } |
| 863 | |
| 864 | // Create and register the connector |
no test coverage detected