Commit be92397
feat: support EXPLAIN ANALYZE on SQL Server for actual execution plans
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.
The classifier accepts an equals sign there (`ANALYZE = false`) and
reads the value after a bare ANALYZE too, so both spellings are
honoured here. Otherwise the layers disagree in the dangerous
direction: the classifier waives its DML check for what it believes is
a non-executing statement, while this connector routes to the path
that executes.
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, guarded by assertNoReadOnlyEscapes
with transactionControl set — the rollback is a real transaction a
COMMIT could close, which is not true of explainQuery. Sharing that
helper also blocks the pass-through data sources it covers: OPENQUERY
and friends execute on the remote source, so a local rollback never
reaches them, and this path executes by design.
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 16 integration tests against a real SQL Server container.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 93b7b7d commit be92397
2 files changed
Lines changed: 428 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
339 | 339 | | |
340 | 340 | | |
341 | 341 | | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
342 | 533 | | |
343 | 534 | | |
344 | 535 | | |
| |||
0 commit comments