A plan that needs a session, and a path that has none
The showplan setting is session-level state and the explain path sends one statement, so the capability is declared false and the action is hidden.
Connect LibreDB Studio to SQL Server, write a query, and look for the Explain action. It is not there. The reason is not that plans are hard to read, and not that nobody has got to the T-SQL dialect yet. It is that a plan on this engine is produced by turning something on in a session, and the code path that would have asked for it can only send one statement.
What a SQL Server execution plan viewer actually requires
On PostgreSQL and MySQL, a plan is a statement. You write EXPLAIN in front of
the query, send the resulting text, and read the rows that come back. The whole
transaction with the server is one request and one response, and a builder that
prepends a keyword to a string is a complete implementation.
SQL Server does not work that way. The provider doc’s own future note for this
gap names the mechanism: SET SHOWPLAN_XML ON (or SET STATISTICS XML ON)
around the statement. The word doing the work is around. A showplan setting is
session state. It is switched on, the statement is then submitted, and the
setting is switched off again - three things in sequence, on one connection,
where the second is only meaningful because the first already happened.
-- three sends in this order, on one connection
SET SHOWPLAN_XML ON;
SELECT o.id, c.name FROM sales.orders o JOIN sales.customers c ON c.id = o.customer_id;
SET SHOWPLAN_XML OFF;
A plan flow here is therefore not a decorated statement. It is a small protocol with a beginning, a middle and an end, and every part of it has to land on the same session.
Why a single-statement path cannot express it
The explain path in the product takes a query and returns a result. It has one slot for SQL and one slot for rows. There is nowhere in that signature to say “and also run this before, and this after, on the same connection, and do not leak the setting to the next query that borrows it”.
Two facts about the SQL Server provider make that shape concrete rather than theoretical.
Connections are pooled. connect() builds an mssql.ConnectionPool and each
query takes a Request from it, with defaults of 2 for pool.min, 10 for
pool.max and a 30-second idle timeout. Session state set by one statement
lives on the connection that ran it, not on the statement. Switch showplan on and hand the
connection back, and the setting outlives the query that asked for it - and the
next borrower of that connection inherits a session it never configured.
Only the first result set comes back. query() reads result.recordset,
singular, so a multi-statement batch or a stored procedure returning several
result sets surfaces just one. Sending the three as a single batch to keep them
together is exactly the kind of thing that returns more than one result set, and
this path is not built to read past the first.
So the honest reading is not “nobody has written the EXPLAIN builder for T-SQL yet”. It is that the builder’s interface - a string in, rows out - cannot carry the thing T-SQL asks for. Widening it is a change to the explain contract on every engine, not a dialect branch.
What the action did before the capability was turned off
The UI’s EXPLAIN builder handles PostgreSQL and MySQL. On SQL Server it had no
dialect to apply, so it applied nothing: before supportsExplain was set to
false, pressing Explain silently ran the unmodified query instead of
returning a plan.
Read that failure carefully, because it is worse than an error. Pressing a button
labelled Explain on an unreviewed statement ran that statement. Against a
SELECT over a large table it was a full execution nobody asked for. The word in
the source note is silently: nothing came back to say a plan had not been
produced.
That family of bug has a sibling on the same provider, which is why it is treated
as a class here rather than a one-off. The T-SQL pagination code once spliced a
TOP into what T-SQL reads as a nested block comment; SQL Server saw the query
without the clause, ran it unbounded, and the method reported wasLimited: true
anyway. Both failures are the same failure: the interface reported an operation
that the server never performed.
The limit, stated plainly: the explain capability on SQL Server is false and
the Explain action is hidden. A real plan needs a session-level showplan setting
wrapped around the statement, which the single-statement explain path cannot
express, and before that flag was flipped the action silently ran the unmodified
query. There is no estimated plan behind a menu here and no partial version of
this feature. It is absent.
Hiding a control as a capability statement
getCapabilities() on this provider returns supportsExplain: false, and the
interface renders from that declaration rather than from a layout guess. That is
the same mechanism described in
the capability declarations behind the feature list - a control that cannot
work is absent, with the reason written where it would have been, rather than
offered and then failed.
Setting the flag was not a workaround for a missing feature. It is what the provider can answer about itself, and it turns a wrong result into a stated absence.
One knock-on worth naming: the model-backed query explainer works by translating
EXPLAIN output into prose. With no EXPLAIN output on this connection there is
nothing for it to translate. The agent’s PLAN mode still opens - it is toolless,
executes nothing, and drafts a statement for a human to run - so it can write you
the showplan sequence above against your real tables, grounded on this provider’s
own inventory. Running it is your decision and your session, which is precisely
the boundary the product cannot cross on your behalf here. AUTO mode does not run
on SQL Server at all; it ends engine-unsupported, because the read-only
execution profile it needs exists only on PostgreSQL, SQLite and DuckDB.
What re-enabling it would take
Not a T-SQL string builder. In order:
- A multi-statement explain path. The explain call has to be able to hold a preamble, the statement and an epilogue, and to guarantee all three run on one connection - the transaction machinery already holds a single pool connection for its lifetime, so the shape exists in the codebase.
- A reader for more than the first result set.
result.recordsetsingular has to become the plural form on this path, or the plan is discarded with the rest of the batch. - A guaranteed reset. The setting has to come off before the connection returns to the pool, including when the statement fails. A pooled connection left in showplan mode is a bug that appears in an unrelated query later.
- Then, and only then, the capability flag.
supportsExplainflips last. The declaration follows the implementation; that ordering is what stops the button from lying again.
The rest of the engine is unaffected by this gap, within its own stated bounds.
Schema introspection is five bulk catalog queries grouped in memory - four over
sys.* views and one over INFORMATION_SCHEMA.COLUMNS. The ER diagram draws
real foreign keys from sys.foreign_keys. Monitoring reads DMVs, with real
blocked-session detection from blocking_session_id and real index usage counts
from dm_db_index_usage_stats, and it is graded partial for a separate reason:
those DMVs need VIEW SERVER STATE, and a login without it gets N/A and empty
lists across the dashboard rather than numbers. The per-engine boundaries,
including this one, are published on the engine pages rather than
discovered when you press something.