Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d8c7946
planner: route single-element information_schema IN predicates like e…
mattlord Sep 1, 2026
2f608c0
planner: only rewrite single-element information_schema IN on routabl…
mattlord Sep 1, 2026
756edf6
planner: extract schema-name list bindvars from information_schema IN…
mattlord Sep 1, 2026
a9d78aa
vtgate: resolve information_schema schema-name IN lists at execution …
mattlord Sep 1, 2026
6978666
e2e: pin information_schema IN predicate routing
mattlord Sep 1, 2026
f03d925
changelog: information_schema IN predicate routing
mattlord Sep 1, 2026
eca04d6
address final review: guard IN rewrite behind translatability, tighte…
mattlord Sep 1, 2026
217a59c
address review: enforce schema cardinality for tuple INs, route singl…
mattlord Sep 2, 2026
08c3062
address review: give routed table-name lists a dedicated bind variable
mattlord Sep 2, 2026
8669b2f
Correct comment
mattlord Sep 2, 2026
2dc5cd6
Address review comments
mattlord Sep 2, 2026
a2667c2
Address Max's comment
mattlord Sep 3, 2026
fba989f
You guessed it....
mattlord Sep 4, 2026
63df5d9
Trim comments and address review comments
mattlord Sep 4, 2026
7e88e30
Fix all-NULL list alongside a scalar
mattlord Sep 4, 2026
356baf5
Another review round
mattlord Sep 4, 2026
bfe7b76
Fix contradictory table-name predicates collapse
mattlord Sep 4, 2026
8dfbe5f
Address more review comments
mattlord Sep 4, 2026
da30809
Leave information_schema IN lists containing database() to the tablet
mattlord Sep 4, 2026
f5f3fa6
Update release note
mattlord Sep 5, 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
34 changes: 34 additions & 0 deletions changelog/25.0/25.0.0/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- **[VTGate](#minor-changes-vtgate)**
- [Ingress bytes in query LogStats](#vtgate-logstats-ingress-bytes)
- [New controls for cross-keyspace reads](#vtgate-cross-keyspace-reads)
- [information_schema queries with IN predicates route correctly](#information-schema-in-routing)
- [Streaming errors no longer surface as connection loss](#vtgate-streamexecute-real-errors)
- [SHA256-hashed passwords in the static gRPC auth plugin](#vtgate-grpc-static-auth-sha256)
- [PREPARE statements no longer report the prepared statement's tables](#vtgate-prepare-tables-used)
Expand Down Expand Up @@ -243,6 +244,39 @@ When enabled, the planner will reject queries that require joining or combining

The VTGate flag prevents cross-keyspace reads globally, regardless of per-keyspace VSchema settings.

#### <a id="information-schema-in-routing"/>`information_schema` queries with IN predicates route correctly</a>
Comment thread
mattlord marked this conversation as resolved.

An `IN` predicate on `table_schema` or `table_name` in an `information_schema`
query previously bypassed schema-name routing entirely and silently returned
an empty or incomplete result (issue #20878) — the form ORMs such as Rails
now generate. A single-valued `IN` (a literal list of one, a bound list with
one value, or a prepared statement's `IN (?)`) now routes exactly like the
equivalent `=` predicate, including routed-table handling for `table_name`.
A multi-value `IN` list routes when it names exactly one schema (duplicates and
`NULL` elements do not count). A list naming more than one schema — in any of
those forms, or an `OR` of schema equalities, which plans identically — cannot
be routed to a single keyspace and now fails with an explicit `VT12001` error
instead of returning wrong rows. Multi-value `table_name` lists are unaffected
and keep working as plain filters, and a list containing `database()` or
`schema()` is left for the tablet to evaluate, as `= database()` always has been.

One narrow shape that previously worked is currently rejected as well: a
multi-value `IN` list naming only system schemas (e.g. `table_schema IN
('information_schema', 'performance_schema')`) now returns the same `VT12001`,
because the routing rewrite cannot carry a multi-schema filter. Split the
query per schema or use equality predicates as a workaround; issue #20974
tracks restoring support for that shape.

Two `=` behaviors change as well. Contradictory predicates on the same
table-name column (`table_name = 'a' AND table_name = 'b'`) previously collapsed
to the last value and returned its rows; they now return the empty result they
describe. A query with several table-name predicates naming routed tables
(routing rules, or tables mid-`MoveTables`) previously honored only whichever
predicate it evaluated first and left the other names unrewritten. Every routed
name is now rewritten, so tables in one keyspace return complete results, and
tables in different keyspaces fail with an explicit `cannot send the query to
multiple keyspace` error instead of routing unpredictably.

#### <a id="vtgate-streamexecute-real-errors"/>Streaming errors no longer surface as connection loss</a>

Streaming queries (under `SET workload = 'OLAP'`, multi-statement batches, and prepared-statement execution) previously returned `ERROR 2013 (HY000): Lost connection to MySQL server during query` and tore down the underlying TCP connection whenever the streaming handler returned an error *after* the first row or field packet had been emitted. VTGate now writes a proper ERR packet in place of the result-set terminator, so the real error code and message reach the client and the connection remains usable for subsequent queries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,43 @@ WHERE TABLE_SCHEMA = 'ks' AND TABLE_NAME = 't2';
)
}

// TestInformationSchemaWithInPredicate reproduces
// https://github.com/vitessio/vitess/issues/20878.
func TestInformationSchemaWithInPredicate(t *testing.T) {
if clusterInstance.HasPartialKeyspaces {
t.Skip("test can randomly select one of the shards, and the shards are in different keyspaces")
}
mcmp, closer := start(t)
defer closer()

eq := utils.Exec(t, mcmp.VtConn, "select table_name from information_schema.tables where table_schema = database() and table_name = 't1'")
in := utils.Exec(t, mcmp.VtConn, "select table_name from information_schema.tables where table_schema in (database()) and table_name in ('t1')")
require.NotEmpty(t, eq.Rows)
require.Equal(t, eq.Rows, in.Rows)

// the issue's literal repro
inLit := utils.Exec(t, mcmp.VtConn, "select table_name from information_schema.tables where table_schema in ('ks') and table_name = 't1'")
require.Equal(t, eq.Rows, inLit.Rows)

_, err := mcmp.VtConn.ExecuteFetch("select table_name from information_schema.tables where table_schema in ('ks', 'other')", 100, false)
require.Error(t, err)
require.Contains(t, err.Error(), "VT12001")

inDup := utils.Exec(t, mcmp.VtConn, "select table_name from information_schema.tables where table_schema in ('ks', 'ks', null) and table_name = 't1'")
require.Equal(t, eq.Rows, inDup.Rows)
utils.AssertResultIsEmpty(t, mcmp.VtConn, "table_schema = 'ks' and table_schema in (null, null)")

// a list containing database() is left for the tablet to evaluate, like = database()
inDB := utils.Exec(t, mcmp.VtConn, "select table_name from information_schema.tables where table_schema in (database(), 'ks') and table_name = 't1'")
require.Equal(t, eq.Rows, inDB.Rows)
inDBSys := utils.Exec(t, mcmp.VtConn, "select table_schema from information_schema.tables where table_schema in (database(), 'information_schema') and table_name in ('t1', 'TABLES')")
require.Len(t, inDBSys.Rows, 2)

// multi-value table_name IN stays a plain filter
multiName := utils.Exec(t, mcmp.VtConn, "select table_name from information_schema.tables where table_schema = database() and table_name in ('t1', 't7_xxhash')")
require.Len(t, multiName.Rows, 2)
}

func TestJoinWithSingleShardQueryOnRHS(t *testing.T) {
// This test checks that we can run queries like this, where the RHS is a single shard query
mcmp, closer := start(t)
Expand Down
247 changes: 247 additions & 0 deletions go/vt/vtgate/engine/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"strconv"
"testing"

"google.golang.org/protobuf/proto"

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

Expand Down Expand Up @@ -198,6 +200,251 @@ func TestInformationSchemaWithTableAndSchemaWithRoutedTables(t *testing.T) {
}
}

func TestSystemTableSchemaLists(t *testing.T) {
tuple := func(name string) evalengine.Expr {
return evalengine.NewBindVarTuple(name, collations.SystemCollation.Collation)
}
literal := func(v string) evalengine.Expr {
return evalengine.NewLiteralString([]byte(v), collations.SystemCollation)
}
replace := sqltypes.Int64BindVariable(1)
tests := []struct {
name string
schemas []evalengine.Expr
bindVars map[string]*querypb.BindVariable
expectedLog []string
wantErr string
}{{
name: "single value routes like equality",
schemas: []evalengine.Expr{tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{"myKeyspace"}),
},
expectedLog: []string{
"ResolveDestinations myKeyspace [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard myKeyspace.1: dummy_select {__replacevtschemaname: %v schemas: %v} false false",
replace, sqltypes.TestBindVariable([]any{"myKeyspace"})),
},
}, {
name: "duplicate values route like equality",
schemas: []evalengine.Expr{tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{"myKeyspace", "myKeyspace"}),
},
expectedLog: []string{
"ResolveDestinations myKeyspace [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard myKeyspace.1: dummy_select {__replacevtschemaname: %v schemas: %v} false false",
replace, sqltypes.TestBindVariable([]any{"myKeyspace", "myKeyspace"})),
},
}, {
name: "NULL elements are ignored",
schemas: []evalengine.Expr{tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{"myKeyspace", nil}),
},
expectedLog: []string{
"ResolveDestinations myKeyspace [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard myKeyspace.1: dummy_select {__replacevtschemaname: %v schemas: %v} false false",
replace, sqltypes.TestBindVariable([]any{"myKeyspace", nil})),
},
}, {
name: "all NULL matches nothing",
schemas: []evalengine.Expr{tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{nil, nil}),
},
expectedLog: []string{
"ResolveDestinations ks [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard ks.1: dummy_select {__vtschemaname: type:VARCHAR schemas: %v} false false",
sqltypes.TestBindVariable([]any{nil, nil})),
},
}, {
name: "distinct values error",
schemas: []evalengine.Expr{tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{"ks1", "ks2"}),
},
wantErr: "VT12001",
}, {
// predicates may be on different schema columns (KEY_COLUMN_USAGE has three)
name: "multi-name list errors even when another predicate names one of them",
schemas: []evalengine.Expr{literal("myKeyspace"), tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{"myKeyspace", "other"}),
},
wantErr: "VT12001",
}, {
name: "list naming no schema makes the query match nothing despite a scalar",
schemas: []evalengine.Expr{literal("myKeyspace"), tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{nil}),
},
expectedLog: []string{
"ResolveDestinations ks [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard ks.1: dummy_select {__vtschemaname: type:VARCHAR schemas: %v} false false",
sqltypes.TestBindVariable([]any{nil})),
},
}, {
name: "scalar and list naming different schemas error",
schemas: []evalengine.Expr{literal("myKeyspace"), tuple("schemas")},
bindVars: map[string]*querypb.BindVariable{
"schemas": sqltypes.TestBindVariable([]any{"ks1"}),
},
wantErr: "specifying two different database in the query is not supported",
}, {
name: "lists agreeing after duplicates and NULLs route",
schemas: []evalengine.Expr{tuple("s1"), tuple("s2")},
bindVars: map[string]*querypb.BindVariable{
"s1": sqltypes.TestBindVariable([]any{"myKeyspace", nil}),
"s2": sqltypes.TestBindVariable([]any{"myKeyspace", "myKeyspace"}),
},
expectedLog: []string{
"ResolveDestinations myKeyspace [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard myKeyspace.1: dummy_select {__replacevtschemaname: %v s1: %v s2: %v} false false",
replace, sqltypes.TestBindVariable([]any{"myKeyspace", nil}), sqltypes.TestBindVariable([]any{"myKeyspace", "myKeyspace"})),
},
}}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
sel := &Route{
RoutingParameters: &RoutingParameters{
Opcode: DBA,
Keyspace: &vindexes.Keyspace{
Name: "ks",
Sharded: false,
},
SysTableTableSchema: tc.schemas,
},
Query: "dummy_select",
FieldQuery: "dummy_select_field",
}
vc := &loggingVCursor{
shards: []string{"1"},
results: []*sqltypes.Result{defaultSelectResult},
}
_, err := sel.TryExecute(t.Context(), vc, tc.bindVars, false)
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
assert.Equal(t, vtrpcpb.Code_UNIMPLEMENTED, vterrors.Code(err))
return
}
require.NoError(t, err)
vc.ExpectLog(t, tc.expectedLog)
})
}
}

// TestSystemTableRoutedTableRewritesEveryTableName pins that every routed
// table-name predicate is rewritten to its physical name, not just the first.
func TestSystemTableRoutedTableRewritesEveryTableName(t *testing.T) {
sel := &Route{
RoutingParameters: &RoutingParameters{
Opcode: DBA,
Keyspace: &vindexes.Keyspace{Name: "ks"},
SysTableTableSchema: []evalengine.Expr{evalengine.NewLiteralString([]byte("schema"), collations.SystemCollation)},
SysTableTableName: map[string]evalengine.Expr{
"t1": evalengine.NewLiteralString([]byte("a"), collations.SystemCollation),
"t2": evalengine.NewLiteralString([]byte("b"), collations.SystemCollation),
},
},
Query: "dummy_select",
FieldQuery: "dummy_select_field",
}
vc := &loggingVCursor{
shards: []string{"1"},
results: []*sqltypes.Result{defaultSelectResult},
tableRoutes: tableRoutes{
tbl: &vindexes.BaseTable{
Name: sqlparser.NewIdentifierCS("routedTable"),
Keyspace: &vindexes.Keyspace{Name: "routedKeyspace"},
},
},
}
bindVars := map[string]*querypb.BindVariable{}

_, err := sel.TryExecute(t.Context(), vc, bindVars, false)
require.NoError(t, err)
want := sqltypes.StringBindVariable("routedTable")
assert.True(t, proto.Equal(want, bindVars["t1"]), "t1 = %v", bindVars["t1"])
assert.True(t, proto.Equal(want, bindVars["t2"]), "t2 = %v", bindVars["t2"])
assert.Contains(t, vc.log, "ResolveDestinations routedKeyspace [] Destinations:DestinationAnyShard()")
}

// sysTableNameInRoute builds a DBA route for `table_name IN ::tables`, keyed by
// the dedicated ::vttables while the expression reads the client's ::tables.
func sysTableNameInRoute() *Route {
return &Route{
RoutingParameters: &RoutingParameters{
Opcode: DBA,
Keyspace: &vindexes.Keyspace{
Name: "ks",
Sharded: false,
},
SysTableTableName: map[string]evalengine.Expr{
"vttables": evalengine.NewBindVarTuple("tables", collations.SystemCollation.Collation),
},
},
Query: "dummy_select",
FieldQuery: "dummy_select_field",
}
}

func TestSystemTableTableNameInSingleValueRoutedRewrite(t *testing.T) {
sel := sysTableNameInRoute()
vc := &loggingVCursor{
shards: []string{"1"},
results: []*sqltypes.Result{defaultSelectResult},
tableRoutes: tableRoutes{
tbl: &vindexes.BaseTable{
Name: sqlparser.NewIdentifierCS("routedTable"),
Keyspace: &vindexes.Keyspace{Name: "routedKeyspace"},
},
},
}
original := sqltypes.TestBindVariable([]any{"tableName"})
bindVars := map[string]*querypb.BindVariable{
"tables": original,
}

_, err := sel.TryExecute(t.Context(), vc, bindVars, false)
require.NoError(t, err)
require.NotNil(t, bindVars["vttables"])
assert.Equal(t, querypb.Type_TUPLE, bindVars["vttables"].Type,
"the rewritten query says `in ::vttables`, so the dedicated bind variable must be a tuple")
assert.True(t, proto.Equal(original, bindVars["tables"]),
"the client's list bind variable may be shared with other predicates and must never be rewritten")
vc.ExpectLog(t, []string{
"FindTable(tableName)",
"ResolveDestinations routedKeyspace [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard routedKeyspace.1: dummy_select {__vtschemaname: type:VARCHAR tables: %v vttables: %v} false false", original, sqltypes.TestBindVariable([]any{"routedTable"})),
})
}

func TestSystemTableTableNameInMultiValueStaysUnrouted(t *testing.T) {
sel := sysTableNameInRoute()
vc := &loggingVCursor{
shards: []string{"1"},
results: []*sqltypes.Result{defaultSelectResult},
}
original := sqltypes.TestBindVariable([]any{"t1", "t2"})
bindVars := map[string]*querypb.BindVariable{
"tables": original,
}

_, err := sel.TryExecute(t.Context(), vc, bindVars, false)
require.NoError(t, err)
assert.True(t, proto.Equal(original, bindVars["tables"]),
"a multi-element table-name list must keep its original bind variable untouched")
require.NotNil(t, bindVars["vttables"],
"the rewritten query references the dedicated variable, so it must be populated even when the list contributes nothing to routing")
assert.Equal(t, original.Values, bindVars["vttables"].Values,
"a multi-element list is copied into the dedicated variable unchanged")
vc.ExpectLog(t, []string{
"ResolveDestinations ks [] Destinations:DestinationAnyShard()",
fmt.Sprintf("ExecuteMultiShard ks.1: dummy_select {__vtschemaname: type:VARCHAR tables: %v vttables: %v} false false", original, bindVars["vttables"]),
})
}

func TestSelectScatter(t *testing.T) {
sel := NewRoute(
Scatter,
Expand Down
Loading
Loading