Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b6e5aac
vtgate: add VEXPLAIN MYSQLPLAN statement
ejortegau Aug 10, 2026
6b9a248
vtgate: make VEXPLAIN MYSQLPLAN support check an allowlist
ejortegau Aug 10, 2026
40aa115
vtgate: test VEXPLAIN MYSQLPLAN rejects INSERT ... SELECT
ejortegau Aug 10, 2026
6a70115
vtgate: mirror no-route fallback in VEXPLAIN MYSQLPLAN
ejortegau Aug 10, 2026
2faefeb
changelog: document VEXPLAIN MYSQLPLAN baseline-plan limitation
ejortegau Aug 10, 2026
12cabf9
vtgate: bind __upper_limit before EXPLAIN for pushed-down scatter limits
ejortegau Aug 10, 2026
42c6562
vtgate: support explicitly targeted Send plans in VEXPLAIN MYSQLPLAN
ejortegau Aug 10, 2026
c83065a
vtadmin: route VEXPLAIN MYSQLPLAN through VExplain result conversion
ejortegau Aug 11, 2026
1e0da3f
vtgate: reject VEXPLAIN MYSQLPLAN in reserved-connection sessions
ejortegau Aug 12, 2026
c55ccbf
vtgate: reject sequence next-value queries in VEXPLAIN MYSQLPLAN
ejortegau Aug 12, 2026
cc71f06
vtgate: bound VEXPLAIN MYSQLPLAN fanout and account for its shard que…
ejortegau Aug 12, 2026
5026e1d
vtgate: copy bind vars per input when collecting MYSQLPLAN tasks
ejortegau Aug 13, 2026
b56aad1
changelog: document VEXPLAIN MYSQLPLAN per-shard EXPLAIN failure beha…
ejortegau Aug 13, 2026
46513b3
vtgate: unify VEXPLAIN plan-description walkers
ejortegau Aug 13, 2026
dd306e7
vtgate: count only successfully-explained shards in MYSQLPLAN accounting
ejortegau Aug 13, 2026
878891b
Merge remote-tracking branch 'upstream/main' into ejortegau/vexplain-…
ejortegau Aug 13, 2026
b811b48
test/e2e: add unsharded keyspace for VEXPLAIN MYSQLPLAN reserved-conn…
ejortegau Aug 14, 2026
cf692e6
vtgate: reject nested query blocks in VEXPLAIN MYSQLPLAN at plan time
ejortegau Aug 17, 2026
b8593e6
vtgate: reject VEXPLAIN MYSQLPLAN sequence queries before bypass plan…
ejortegau Aug 17, 2026
0b6608e
vtgate: count completed shards when a MYSQLPLAN EXPLAIN fails
ejortegau Aug 17, 2026
f8fc5c1
vtgate: count every attempted shard in MYSQLPLAN accounting
ejortegau Aug 18, 2026
5ef7ad2
vtgate: reject VEXPLAIN MYSQLPLAN advisory lock functions
ejortegau Aug 18, 2026
bc3d25b
vtgate: don't pin a VEXPLAIN MYSQLPLAN session for a SET_VAR-only sysvar
ejortegau Aug 19, 2026
2b9f32f
vtgate: reject VEXPLAIN MYSQLPLAN of SQL_CALC_FOUND_ROWS with GROUP B…
ejortegau Aug 19, 2026
0355407
docs: note VEXPLAIN MYSQLPLAN does not table-ACL-check per-shard EXPL…
ejortegau Aug 19, 2026
e52ccf9
vtgate: give VEXPLAIN MYSQLPLAN derived-table/view rejection an hones…
ejortegau Aug 19, 2026
04dbb37
vtgate: make MYSQLPLAN shard-query accounting an optional VCursor cap…
ejortegau Aug 19, 2026
bdca6b0
vtgate: run VEXPLAIN MYSQLPLAN EXPLAINs through the shared scatter path
ejortegau Aug 20, 2026
6cdbdda
vtgate: drop dead MergeSort entry from VEXPLAIN MYSQLPLAN allowlist
ejortegau Aug 20, 2026
ce2eef5
test/e2e: probe MySQL version from the sharded keyspace by name
ejortegau Aug 20, 2026
421d6f3
test/e2e: cover VEXPLAIN MYSQLPLAN scatter fan-out across shards
ejortegau Aug 20, 2026
afb986f
vtgate: rename MYSQLPLAN output key to mysql_explain_json_by_shard
ejortegau Aug 20, 2026
5880984
vtgate: fold VEXPLAIN MYSQLPLAN AST checks into one walk
ejortegau Aug 20, 2026
ac3f349
vtgate: reject VEXPLAIN MYSQLPLAN DML by statement type
ejortegau Aug 20, 2026
e5cd381
vtgate: move mysqlExplainTask into the top type() block
ejortegau Aug 20, 2026
44077e6
vtgate: warn when a MYSQLPLAN shard returns an empty EXPLAIN result
ejortegau Aug 20, 2026
eaee2cc
changelog: note VEXPLAIN MYSQLPLAN in-transaction reads pre-tx state
ejortegau Aug 20, 2026
aa5d20d
test/e2e: assert VEXPLAIN MYSQLPLAN of INTO OUTFILE/DUMPFILE has no s…
ejortegau Aug 20, 2026
a332f0b
changelog: clarify VEXPLAIN MYSQLPLAN concurrency is per-shard, not p…
ejortegau Aug 21, 2026
fa74d13
vtgate: give lock/sequence rejection priority in VEXPLAIN MYSQLPLAN A…
ejortegau Aug 21, 2026
a4c5304
changelog: note table-scoped query denylist rules also bypass MYSQLPL…
ejortegau Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions changelog/25.0/25.0.0/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [Preparing a statement no longer starts an implicit transaction](#vtgate-prepare-no-implicit-tx)
- [Stricter validation of SQL-level PREPARE statements](#vtgate-prepare-stricter-validation)
- [Stricter PROXY protocol v1 header validation](#vtgate-proxy-protocol-v1-strictness)
- [New `VEXPLAIN MYSQLPLAN` statement](#vtgate-vexplain-mysqlplan)
- **[Reparent](#minor-changes-reparent)**
- [`EmergencyReparentShard` no longer waits on replicas that cannot win the election](#ers-lagging-relay-log-wait)
- [`EmergencyReparentShard` can explicitly recover from split brain](#ers-allow-split-brain-promotion)
Expand Down Expand Up @@ -268,6 +269,18 @@ Specification-conformant v1 headers, as emitted by HAProxy, AWS load balancers,

See [#20733](https://github.com/vitessio/vitess/pull/20733) for details.

#### <a id="vtgate-vexplain-mysqlplan"/>New `VEXPLAIN MYSQLPLAN` statement</a>

A new `VEXPLAIN MYSQLPLAN <query>` statement runs MySQL's `EXPLAIN FORMAT=JSON` against the shards a `SELECT` would target, **without executing the query itself**. It resolves each `Route`'s target shards from its vindex at resolution time and issues `EXPLAIN` against every resolved shard, attaching the per-shard MySQL plan to the VTGate plan tree keyed by shard, so per-shard plan and cost differences are visible.

Unlike `VEXPLAIN ALL`, which executes the query to discover the shard-level queries before explaining them, `VEXPLAIN MYSQLPLAN` never runs the wrapped query.

Only `SELECT` statements whose target shards can be resolved from a vindex without reading cluster data are supported. DML (`INSERT`/`UPDATE`/`DELETE`), and any query whose shard set depends on data — cross-shard joins, subqueries, recursive CTEs, and lookup vindexes — are rejected with an error suggesting `VEXPLAIN ALL` instead.

For queries eligible for deferred plan optimization (where equal bind variable values let the plan collapse to a single shard at execution time), `VEXPLAIN MYSQLPLAN` explains the general (baseline) plan rather than the value-specific optimized one, so it reports the full shard footprint the query can target regardless of the bind variable values supplied.

The per-shard `EXPLAIN` queries are run concurrently. If the `EXPLAIN` against any targeted shard fails (for example, an unreachable shard), the whole `VEXPLAIN MYSQLPLAN` command fails with that error rather than returning a partial result — matching the default all-or-nothing behavior of a scatter query.

### <a id="minor-changes-reparent"/>Reparent</a>

#### <a id="ers-lagging-relay-log-wait"/>`EmergencyReparentShard` no longer waits on replicas that cannot win the election</a>
Expand Down
21 changes: 21 additions & 0 deletions go/test/endtoend/vtgate/queries/vexplain/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
clusterInstance *cluster.LocalProcessCluster
vtParams mysql.ConnParams
shardedKs = "ks"
unshardedKs = "uks"

shardedKsShards = []string{"-40", "40-80", "80-c0", "c0-"}
Cell = "test"
Expand All @@ -40,6 +41,12 @@ var (

//go:embed vschema.json
shardedVSchema string

//go:embed unsharded_schema.sql
unshardedSchemaSQL string

//go:embed unsharded_vschema.json
unshardedVSchema string
)

func TestMain(m *testing.M) {
Expand All @@ -55,6 +62,20 @@ func TestMain(m *testing.M) {
return 1
}

// Start unsharded keyspace. VEXPLAIN MYSQLPLAN's reserved-connection test
// creates a temporary table, which Vitess only allows on an unsharded
// keyspace.
uKs := &cluster.Keyspace{
Name: unshardedKs,
SchemaSQL: unshardedSchemaSQL,
VSchema: unshardedVSchema,
}

err = clusterInstance.StartUnshardedKeyspace(*uKs, 0, false, clusterInstance.Cell)
if err != nil {
return 1
}

// Start keyspace
sKs := &cluster.Keyspace{
Name: shardedKs,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
create table u_user
(
id bigint,
primary key (id)
) Engine = InnoDB;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sharded": false,
"tables": {
"u_user": {}
}
}
74 changes: 74 additions & 0 deletions go/test/endtoend/vtgate/queries/vexplain/vexplain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ package vexplain

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"vitess.io/vitess/go/mysql"
"vitess.io/vitess/go/mysql/capabilities"
"vitess.io/vitess/go/sqltypes"
"vitess.io/vitess/go/test/endtoend/onlineddl"
"vitess.io/vitess/go/test/endtoend/utils"
)

Expand Down Expand Up @@ -149,3 +152,74 @@ func TestVExplainAll(t *testing.T) {
utils.AssertMatchesContains(t, conn, `vexplain /*vt+ EXECUTE_DML_QUERIES */ all insert into user (id,lookup,lookup_unique) values (4,'apa','foo'),(5,'apa','bar'),(6,'monkey','nobar')`, "Insert", "mysql_explain_json")
utils.AssertMatchesContains(t, conn, `vexplain all select id from user where lookup = "apa"`, "mysql_explain_json", "ByDestination")
}

func TestVExplainMySQLPlan(t *testing.T) {
// VEXPLAIN MYSQLPLAN is a v25 syntax; an older vtgate cannot parse it. Under the
// upgrade/downgrade CI this suite can run current test code against an N-1 vtgate.
utils.SkipIfBinaryIsBelowVersion(t, 25, "vtgate")

conn, closer := start(t)
defer closer()

// The result must carry the VTGate plan tree (Route) with real per-shard MySQL
// EXPLAIN output attached, without executing the query.
utils.AssertMatchesContains(t, conn,
`vexplain mysqlplan select id from user where id = 1`,
"mysql_explain_json", "Route")

// query_block is a key emitted only by a genuine MySQL EXPLAIN FORMAT=JSON, so it
// proves we actually reached MySQL. MariaDB's EXPLAIN JSON does not use it, so gate
// this assertion to MySQL/Percona 8.0+ (which covers the 8.0 and 8.4 CI flavors).
mysqlVersion := onlineddl.GetMySQLVersion(t, clusterInstance.Keyspaces[0].Shards[0].PrimaryTablet())
require.NotEmpty(t, mysqlVersion)
atLeast80, err := capabilities.ServerVersionAtLeast(mysqlVersion, 8, 0, 0)
require.NoError(t, err)
if atLeast80 && !strings.Contains(mysqlVersion, "MariaDB") {
utils.AssertMatchesContains(t, conn,
`vexplain mysqlplan select id from user where id = 1`,
"query_block")
}

// DML is not supported (its plans are not Route primitives): it must fail closed
// and point the user to VEXPLAIN ALL, not silently produce a plan with no EXPLAIN.
utils.AssertContainsError(t, conn,
`vexplain mysqlplan insert into user (id,lookup,lookup_unique) values (99,'apa','apa')`,
"use VEXPLAIN ALL instead")

// A lookup vindex cannot resolve shards without executing, so it must fail closed
// and point the user to VEXPLAIN ALL.
utils.AssertContainsError(t, conn,
`vexplain mysqlplan select id from user where lookup_unique = "apa"`,
"use VEXPLAIN ALL instead")
}

// TestVExplainMySQLPlanReservedConn verifies that once a session holds a reserved
// connection (here, by creating a temporary table), VEXPLAIN MYSQLPLAN fails
// closed rather than reporting a plan from a separate connection that cannot see
// the session's temporary tables. The plain SELECT still succeeds on the reserved
// connection, which is exactly the asymmetry the rejection guards against.
func TestVExplainMySQLPlanReservedConn(t *testing.T) {
utils.SkipIfBinaryIsBelowVersion(t, 25, "vtgate")

// A dedicated connection, since creating a temp table pins it to a reserved
// connection for the rest of its life.
ctx := t.Context()
conn, err := mysql.Connect(ctx, &vtParams)
require.NoError(t, err)
defer conn.Close()

// Temporary tables are only allowed on an unsharded keyspace.
utils.Exec(t, conn, "use "+unshardedKs)
utils.Exec(t, conn, `create temporary table temp_user(id bigint primary key)`)
utils.Exec(t, conn, `insert into temp_user(id) values (1)`)

// The real SELECT works on the reserved connection that can see the temp table.
utils.AssertMatches(t, conn, `select id from temp_user`, `[[INT64(1)]]`)

// But MYSQLPLAN must refuse rather than EXPLAIN on a connection that cannot see
// the temp table. It must not point the user at VEXPLAIN ALL, which shares the
// same standalone-EXPLAIN blind spot.
_, err = utils.ExecAllowError(t, conn, `vexplain mysqlplan select id from temp_user`)
require.ErrorContains(t, err, "reserved connection")
require.NotContains(t, err.Error(), "VEXPLAIN ALL")
}
2 changes: 2 additions & 0 deletions go/vt/sqlparser/ast_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,8 @@ func (ty VExplainType) ToString() string {
return TraceStr
case KeysVExplainType:
return KeysStr
case MySQLVExplainType:
return MySQLStr
default:
return "Unknown VExplainType"
}
Expand Down
2 changes: 2 additions & 0 deletions go/vt/sqlparser/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ const (
PlanStr = "plan"
TraceStr = "trace"
KeysStr = "keys"
MySQLStr = "mysqlplan"

// Lock Types
ReadStr = "read"
Expand Down Expand Up @@ -864,6 +865,7 @@ const (
AllVExplainType
TraceVExplainType
KeysVExplainType
MySQLVExplainType
Comment thread
ejortegau marked this conversation as resolved.
)

// Constant for Enum Type - SelectIntoType
Expand Down
1 change: 1 addition & 0 deletions go/vt/sqlparser/keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ var keywords = []keyword{
{"multipoint", MULTIPOINT},
{"multipolygon", MULTIPOLYGON},
{"month", MONTH},
{"mysqlplan", MYSQLPLAN},
{"mysql_errno", MYSQL_ERRNO},
{"name", NAME},
{"names", NAMES},
Expand Down
2 changes: 2 additions & 0 deletions go/vt/sqlparser/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2802,6 +2802,8 @@ var validSQL = []struct {
input: "vexplain trace select * from t",
}, {
input: "vexplain keys select * from t",
}, {
input: "vexplain mysqlplan select * from t",
}, {
input: "explain analyze select * from t",
}, {
Expand Down
Loading
Loading