Skip to content

feat: support EXPLAIN ANALYZE on SQL Server for actual execution plans#361

Open
DANze11 wants to merge 1 commit into
bytebase:mainfrom
DANze11:feat/sqlserver-explain-analyze
Open

feat: support EXPLAIN ANALYZE on SQL Server for actual execution plans#361
DANze11 wants to merge 1 commit into
bytebase:mainfrom
DANze11:feat/sqlserver-explain-analyze

Conversation

@DANze11

@DANze11 DANze11 commented Jul 16, 2026

Copy link
Copy Markdown

Problem

On SQL Server, EXPLAIN maps to SET SHOWPLAN_XML, which compiles a statement
without executing it. The resulting plan carries estimates only — there is no way
to get a plan with real row counts, which is what you need when the estimates are
the problem.

There is also an existing gap. The read-only classifier already recognises
EXPLAIN ANALYZE (it is a PostgreSQL form, see utils/allowed-keywords.ts) and
correctly refuses it for DML, but the SQL Server connector has no handling for it.
Both spellings currently break:

EXPLAIN ANALYZE SELECT ...    -- reaches the server as `ANALYZE SELECT ...`
EXPLAIN (ANALYZE) SELECT ...  -- reaches the server as `(ANALYZE) SELECT ...`

Both are syntax errors.

Solution

EXPLAIN ANALYZE maps to SET STATISTICS XML, which executes the statement and
returns a plan carrying ActualRows / ActualExecutions — the PostgreSQL
EXPLAIN ANALYZE contract this connector already emulates for plain EXPLAIN.

EXPLAIN SELECT ...          -- estimated plan (unchanged)
EXPLAIN ANALYZE SELECT ...  -- actual plan, real row counts

Notes on the implementation

Plan extraction reads the last recordset, not recordset (singular).
STATISTICS XML interleaves each statement's plan with that statement's own
result sets, so the plan sits at no fixed index — the singular accessor would hand
back the query's data instead of the plan.

Option parsing covers the forms the classifier already recognises: ANALYZE,
ANALYZE VERBOSE, (ANALYZE), (ANALYZE, ...), (ANALYZE false).
PostgreSQL-only options (BUFFERS, VERBOSE, COSTS, …) have no SQL Server
counterpart and are refused by name rather than silently dropped — quietly
ignoring an option would return something other than what was asked for. A
disabled ANALYZE (false/off/0) falls back to the estimated plan, mirroring
the classifier's own negative lookahead.

Read-only is handled explicitly. Unlike EXPLAIN, EXPLAIN ANALYZE executes,
so it is not inherently read-only the way explainQuery() is. Under
options.readonly the statement runs inside a transaction that always rolls back,
reusing the reasoning and keyword guards of executeReadOnly() (#342, #350).

Session isolation is preserved. The SET STATISTICS XML toggle runs on the
same short-lived single-connection pool explainQuery() already uses, so the
session state cannot leak onto a shared pool connection where a concurrent query
would inherit it.

Tests

12 new integration tests against a real SQL Server container, covering: actual
plan contents (RunTimeInformation / ActualRows), that the statement really
executes (unlike EXPLAIN), rollback under readonly, the parenthesized forms,
disabled ANALYZE falling back to an estimated plan, refusal of PostgreSQL-only
options, and the malformed cases.

Tests  62 passed (62)

The full sqlserver.integration.test.ts suite passes (50 pre-existing + 12 new).
No changes to other connectors; pnpm test:unit is unchanged.

A plain EXPLAIN maps to SET SHOWPLAN_XML, which compiles the statement
without executing it — so its plan carries estimates only. There was no
way to get a plan with real row counts.

EXPLAIN ANALYZE now maps to SET STATISTICS XML, which runs the statement
and returns a plan carrying ActualRows/ActualExecutions, matching the
PostgreSQL contract the connector already emulates for EXPLAIN.

This also fixes an existing gap: the read-only classifier already
recognises EXPLAIN ANALYZE (allowed-keywords.ts) and correctly refuses it
for DML, but the SQL Server connector had no handling for it. Both
`EXPLAIN ANALYZE <query>` and `EXPLAIN (ANALYZE) <query>` fell through to
explainQuery() and reached the server as `ANALYZE <query>` /
`(ANALYZE) <query>` — a syntax error.

Details:

1. Plan extraction reads the last recordset, not `recordset` (singular).
   STATISTICS XML interleaves each statement's plan with that statement's
   own result sets, so the plan sits at no fixed index and the singular
   accessor would hand back the query's data instead.

2. Option parsing covers the forms the classifier recognises: ANALYZE,
   ANALYZE VERBOSE, (ANALYZE), (ANALYZE, ...), (ANALYZE false).
   PostgreSQL-only options (BUFFERS, VERBOSE, COSTS, ...) have no SQL
   Server counterpart and are refused by name — silently dropping an
   option would quietly return something other than what was asked for.
   A disabled ANALYZE (false/off/0) falls back to the estimated plan,
   mirroring the classifier's own lookahead.

3. Unlike EXPLAIN, EXPLAIN ANALYZE executes, so it is not inherently
   read-only. Under options.readonly the statement runs inside a
   transaction that always rolls back, reusing the reasoning and keyword
   guards of executeReadOnly().

4. The SET STATISTICS XML toggle runs on the same short-lived
   single-connection pool explainQuery() already uses, so the session
   state cannot leak onto a shared pool connection where a concurrent
   query would inherit it.

Adds 12 integration tests against a real SQL Server container.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SQL Server support for PostgreSQL-style EXPLAIN ANALYZE by translating it to SET STATISTICS XML, enabling actual execution plans (with real row counts) while preserving read-only safety via rollback-on-complete execution when options.readonly is enabled.

Changes:

  • Extend SQL Server executeSQL() EXPLAIN handling to parse EXPLAIN modifiers and route EXPLAIN ANALYZE to a new SET STATISTICS XML execution path.
  • Implement plan extraction from STATISTICS XML results by scanning the final recordsets for a ShowPlanXML payload.
  • Add SQL Server integration tests covering EXPLAIN ANALYZE semantics, readonly rollback behavior, supported/unsupported option forms, and malformed inputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/connectors/sqlserver/index.ts Adds EXPLAIN option parsing and EXPLAIN ANALYZE execution-plan support via SET STATISTICS XML, including readonly rollback handling and plan extraction.
src/connectors/tests/sqlserver.integration.test.ts Adds SQL Server integration coverage for actual-plan generation, execution vs. compile-only behavior, and option/error handling.

Comment on lines +898 to +906
const parsed = /^([A-Za-z_]+)(?:\s+(\S+))?$/.exec(token);
const name = parsed?.[1];
if (!name || !/^analyze$/i.test(name)) {
throw new Error(
`EXPLAIN option '${name ?? token}' is not supported on SQL Server — only ANALYZE is.`
);
}

const value = parsed[2];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants