From 6564ad8d11d59c124a3cff6e9908aba69fa53e28 Mon Sep 17 00:00:00 2001 From: Mohamed Hamza Date: Tue, 6 Jan 2026 18:06:55 -0500 Subject: [PATCH 1/3] Add support for view routing rules Adds support for view routing rules. View routing rules can be applied in the same way as tables: ```json { "rules": [ { "from_table": "my_view", "to_tables": ["target_ks.my_view"] } ] } ``` When routing rules are being built in VTGate, the `from_table` is first checked to see if it's a view. If it is, it's adding into the local VSchema in a new `ViewRoutingRules` field: ```go type VSchema struct { // ... RoutingRules map[string]*RoutingRule `json:"routing_rules"` ViewRoutingRules map[string]*ViewRoutingRule `json:"view_routing_rules"` // ... } ``` A view routing rule is defined as: ```go type ViewRoutingRule struct { TargetKeyspace string TargetViewName string } ``` At query time, views are rewritten into the target view's definition rather than the source view. For example, for this view: ```sql create view user_view as select id, name from user ``` And this routing rule: ```json { "rules": [ { "from_table": "user_view", "to_tables": ["target_ks.user_view"] } ] } ``` The query: ```sql select * from user_view ``` Would be rewritten into: ```sql select * from (select col1, col2 from target_ks.user_view) as user_view ``` See the [RFC](https://github.com/vitessio/vitess/issues/19097) for more information. Signed-off-by: Mohamed Hamza --- .../view_routing_rules/source_schema.sql | 7 + .../view_routing_rules/source_vschema.json | 6 + .../view_routing_rules/target_schema.sql | 7 + .../view_routing_rules/target_vschema.json | 18 ++ .../view_routing_rules/vrr_test.go | 169 ++++++++++++++++++ go/test/vschemawrapper/vschema_wrapper.go | 6 +- go/vt/sqlparser/normalizer.go | 16 +- go/vt/sqlparser/normalizer_test.go | 8 +- go/vt/vtgate/executorcontext/vcursor_impl.go | 6 +- .../plancontext/planning_context_test.go | 2 +- .../vtgate/planbuilder/plancontext/vschema.go | 2 +- go/vt/vtgate/vindexes/vschema.go | 78 +++++++- go/vt/vtgate/vindexes/vschema_routing_test.go | 1 + go/vt/vtgate/vindexes/vschema_test.go | 33 ++-- go/vt/vtgate/vschema_manager.go | 3 + go/vt/vtgate/vschema_manager_test.go | 116 ++++++++++-- 16 files changed, 432 insertions(+), 46 deletions(-) create mode 100644 go/test/endtoend/vtgate/schematracker/view_routing_rules/source_schema.sql create mode 100644 go/test/endtoend/vtgate/schematracker/view_routing_rules/source_vschema.json create mode 100644 go/test/endtoend/vtgate/schematracker/view_routing_rules/target_schema.sql create mode 100644 go/test/endtoend/vtgate/schematracker/view_routing_rules/target_vschema.json create mode 100644 go/test/endtoend/vtgate/schematracker/view_routing_rules/vrr_test.go diff --git a/go/test/endtoend/vtgate/schematracker/view_routing_rules/source_schema.sql b/go/test/endtoend/vtgate/schematracker/view_routing_rules/source_schema.sql new file mode 100644 index 00000000000..34c13c3e1ca --- /dev/null +++ b/go/test/endtoend/vtgate/schematracker/view_routing_rules/source_schema.sql @@ -0,0 +1,7 @@ +create table t1 ( + id bigint, + val varchar(100), + primary key(id) +) Engine=InnoDB; + +create view view1 as select id, val from t1; diff --git a/go/test/endtoend/vtgate/schematracker/view_routing_rules/source_vschema.json b/go/test/endtoend/vtgate/schematracker/view_routing_rules/source_vschema.json new file mode 100644 index 00000000000..1d9ff29a4ba --- /dev/null +++ b/go/test/endtoend/vtgate/schematracker/view_routing_rules/source_vschema.json @@ -0,0 +1,6 @@ +{ + "sharded": false, + "tables": { + "t1": {} + } +} diff --git a/go/test/endtoend/vtgate/schematracker/view_routing_rules/target_schema.sql b/go/test/endtoend/vtgate/schematracker/view_routing_rules/target_schema.sql new file mode 100644 index 00000000000..34c13c3e1ca --- /dev/null +++ b/go/test/endtoend/vtgate/schematracker/view_routing_rules/target_schema.sql @@ -0,0 +1,7 @@ +create table t1 ( + id bigint, + val varchar(100), + primary key(id) +) Engine=InnoDB; + +create view view1 as select id, val from t1; diff --git a/go/test/endtoend/vtgate/schematracker/view_routing_rules/target_vschema.json b/go/test/endtoend/vtgate/schematracker/view_routing_rules/target_vschema.json new file mode 100644 index 00000000000..1eca6978b27 --- /dev/null +++ b/go/test/endtoend/vtgate/schematracker/view_routing_rules/target_vschema.json @@ -0,0 +1,18 @@ +{ + "sharded": true, + "vindexes": { + "xxhash": { + "type": "xxhash" + } + }, + "tables": { + "t1": { + "column_vindexes": [ + { + "column": "id", + "name": "xxhash" + } + ] + } + } +} diff --git a/go/test/endtoend/vtgate/schematracker/view_routing_rules/vrr_test.go b/go/test/endtoend/vtgate/schematracker/view_routing_rules/vrr_test.go new file mode 100644 index 00000000000..e410211f7ec --- /dev/null +++ b/go/test/endtoend/vtgate/schematracker/view_routing_rules/vrr_test.go @@ -0,0 +1,169 @@ +/* +Copyright 2025 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package viewroutingrules + +import ( + "context" + _ "embed" + "flag" + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "vitess.io/vitess/go/mysql" + "vitess.io/vitess/go/test/endtoend/cluster" + "vitess.io/vitess/go/test/endtoend/utils" +) + +var ( + clusterInstance *cluster.LocalProcessCluster + vtParams mysql.ConnParams + sourceKs = "source_ks" + targetKs = "target_ks" + cell = "zone1" + + //go:embed source_schema.sql + sourceSchema string + + //go:embed target_schema.sql + targetSchema string + + //go:embed source_vschema.json + sourceVSchema string + + //go:embed target_vschema.json + targetVSchema string +) + +func TestMain(m *testing.M) { + flag.Parse() + + exitCode := func() int { + clusterInstance = cluster.NewCluster(cell, "localhost") + defer clusterInstance.Teardown() + + // Start topo server. + err := clusterInstance.StartTopo() + if err != nil { + return 1 + } + + // Enable views tracking. + clusterInstance.VtGateExtraArgs = append(clusterInstance.VtGateExtraArgs, "--enable-views") + clusterInstance.VtTabletExtraArgs = append(clusterInstance.VtTabletExtraArgs, "--queryserver-enable-views") + + // Start source keyspace (unsharded, with 1 replica for tablet type routing test). + sks := cluster.Keyspace{ + Name: sourceKs, + SchemaSQL: sourceSchema, + VSchema: sourceVSchema, + } + err = clusterInstance.StartUnshardedKeyspace(sks, 1, false, cell) + if err != nil { + return 1 + } + + // Start target keyspace (sharded, with 1 replica for tablet type routing test). + tks := cluster.Keyspace{ + Name: targetKs, + SchemaSQL: targetSchema, + VSchema: targetVSchema, + } + err = clusterInstance.StartKeyspace(tks, []string{"-80", "80-"}, 1, false, cell) + if err != nil { + return 1 + } + + // Start vtgate. + err = clusterInstance.StartVtgate() + if err != nil { + return 1 + } + + err = clusterInstance.WaitForVTGateAndVTTablets(1 * time.Minute) + if err != nil { + fmt.Println(err) + return 1 + } + + vtParams = mysql.ConnParams{ + Host: clusterInstance.Hostname, + Port: clusterInstance.VtgateMySQLPort, + } + return m.Run() + }() + os.Exit(exitCode) +} + +func TestViewRoutingRules(t *testing.T) { + ctx := context.Background() + conn, err := mysql.Connect(ctx, &vtParams) + require.NoError(t, err) + defer conn.Close() + + // Wait for views to be tracked by the schema tracker. + viewExists := func(t *testing.T, ksMap map[string]any) bool { + views, ok := ksMap["views"] + if !ok { + return false + } + viewsMap := views.(map[string]any) + _, ok = viewsMap["view1"] + return ok + } + utils.WaitForVschemaCondition(t, clusterInstance.VtgateProcess, sourceKs, viewExists, "source view1 not found") + utils.WaitForVschemaCondition(t, clusterInstance.VtgateProcess, targetKs, viewExists, "target view1 not found") + + // Insert different data in each keyspace so we can distinguish which one is being queried. + utils.Exec(t, conn, "insert into source_ks.t1(id, val) values(1, 'source_data')") + utils.Exec(t, conn, "insert into target_ks.t1(id, val) values(1, 'target_data')") + + utils.AssertMatches(t, conn, "select * from source_ks.view1", `[[INT64(1) VARCHAR("source_data")]]`) + utils.AssertMatches(t, conn, "select * from target_ks.view1", `[[INT64(1) VARCHAR("target_data")]]`) + + // Unqualified query should fail with ambiguous table error since view1 exists in both keyspaces. + _, err = utils.ExecAllowError(t, conn, "select * from view1") + require.Error(t, err) + require.Contains(t, err.Error(), "ambiguous") + + // Apply routing rules to route to target + routingRules := `{"rules": [ + {"from_table": "view1", "to_tables": ["target_ks.view1"]}, + {"from_table": "source_ks.view1", "to_tables": ["target_ks.view1"]}, + {"from_table": "source_ks.view1@replica", "to_tables": ["target_ks.view1"]} + ]}` + err = clusterInstance.VtctldClientProcess.ApplyRoutingRules(routingRules) + require.NoError(t, err) + defer func() { + err := clusterInstance.VtctldClientProcess.ApplyRoutingRules("{}") + require.NoError(t, err) + }() + + // After routing rules, all queries should return target data. + + utils.AssertMatches(t, conn, "select * from view1", `[[INT64(1) VARCHAR("target_data")]]`) + utils.AssertMatches(t, conn, "select * from source_ks.view1", `[[INT64(1) VARCHAR("target_data")]]`) + + utils.Exec(t, conn, "use @replica") + utils.AssertMatches(t, conn, "select * from source_ks.view1", `[[INT64(1) VARCHAR("target_data")]]`) + + utils.Exec(t, conn, "use @primary") + utils.AssertMatches(t, conn, "select * from target_ks.view1", `[[INT64(1) VARCHAR("target_data")]]`) +} diff --git a/go/test/vschemawrapper/vschema_wrapper.go b/go/test/vschemawrapper/vschema_wrapper.go index 4a526b434a6..ee009658adc 100644 --- a/go/test/vschemawrapper/vschema_wrapper.go +++ b/go/test/vschemawrapper/vschema_wrapper.go @@ -247,12 +247,12 @@ func (vw *VSchemaWrapper) FindTable(tab sqlparser.TableName) (*vindexes.BaseTabl return table, destKeyspace, destTabletType, destTarget, nil } -func (vw *VSchemaWrapper) FindView(tab sqlparser.TableName) sqlparser.TableStatement { +func (vw *VSchemaWrapper) FindView(tab sqlparser.TableName) (sqlparser.TableStatement, *sqlparser.TableName) { destKeyspace, _, _, err := topoproto.ParseDestination(tab.Qualifier.String(), topodatapb.TabletType_PRIMARY) if err != nil { - return nil + return nil, nil } - return vw.V.FindView(destKeyspace, tab.Name.String()) + return vw.V.FindView(destKeyspace, tab.Name.String()), nil } func (vw *VSchemaWrapper) FindViewTarget(name sqlparser.TableName) (*vindexes.Keyspace, error) { diff --git a/go/vt/sqlparser/normalizer.go b/go/vt/sqlparser/normalizer.go index 6df900fca13..5579ce32a99 100644 --- a/go/vt/sqlparser/normalizer.go +++ b/go/vt/sqlparser/normalizer.go @@ -73,7 +73,7 @@ type ( } // VSchemaViews provides access to view definitions within the VSchema. VSchemaViews interface { - FindView(name TableName) TableStatement + FindView(name TableName) (TableStatement, *TableName) } ) @@ -619,11 +619,23 @@ func (nz *normalizer) rewriteAliasedTable(cursor *Cursor, node *AliasedTableExpr } // Replace views with their underlying definitions. + nz.rewriteView(aliasTableName, node, tblName) +} + +func (nz *normalizer) rewriteView(aliasTableName TableName, node *AliasedTableExpr, tblName string) { if nz.views == nil { return } - view := nz.views.FindView(aliasTableName) + + view, targetViewName := nz.views.FindView(aliasTableName) if view == nil { + // If a view routing rule matched this view, but the target view was not found, we'll rewrite the view name + // into the target view so that any error messages reference the target view, e.g. "table 'target_view' + // does not exist in keyspace 'target_ks'", rather than silently continuing with the source view. + if targetViewName != nil { + node.Expr = *targetViewName + } + return } diff --git a/go/vt/sqlparser/normalizer_test.go b/go/vt/sqlparser/normalizer_test.go index 0b426ff148c..bca6115063e 100644 --- a/go/vt/sqlparser/normalizer_test.go +++ b/go/vt/sqlparser/normalizer_test.go @@ -947,16 +947,16 @@ func TestRewrites(in *testing.T) { type fakeViews struct{} -func (*fakeViews) FindView(name TableName) TableStatement { +func (*fakeViews) FindView(name TableName) (TableStatement, *TableName) { if name.Name.String() != "user_details" { - return nil + return nil, nil } parser := NewTestParser() statement, err := parser.Parse("select user.id, user.name, user_extra.salary from user join user_extra where user.id = user_extra.user_id") if err != nil { - return nil + return nil, nil } - return statement.(TableStatement) + return statement.(TableStatement), nil } func TestRewritesWithSetVarComment(in *testing.T) { diff --git a/go/vt/vtgate/executorcontext/vcursor_impl.go b/go/vt/vtgate/executorcontext/vcursor_impl.go index fc98e1f37c0..10671eb49ba 100644 --- a/go/vt/vtgate/executorcontext/vcursor_impl.go +++ b/go/vt/vtgate/executorcontext/vcursor_impl.go @@ -465,15 +465,15 @@ func (vc *VCursorImpl) FindTable(name sqlparser.TableName) (*vindexes.BaseTable, return table, destKeyspace, destTabletType, dest, err } -func (vc *VCursorImpl) FindView(name sqlparser.TableName) sqlparser.TableStatement { +func (vc *VCursorImpl) FindView(name sqlparser.TableName) (sqlparser.TableStatement, *sqlparser.TableName) { ks, _, _, err := vc.parseDestinationTarget(name.Qualifier.String()) if err != nil { - return nil + return nil, nil } if ks == "" { ks = vc.keyspace } - return vc.vschema.FindView(ks, name.Name.String()) + return vc.vschema.FindRoutedView(ks, name.Name.String(), vc.tabletType) } func (vc *VCursorImpl) FindRoutedTable(name sqlparser.TableName) (*vindexes.BaseTable, error) { diff --git a/go/vt/vtgate/planbuilder/plancontext/planning_context_test.go b/go/vt/vtgate/planbuilder/plancontext/planning_context_test.go index 16cf9582766..b98c313847b 100644 --- a/go/vt/vtgate/planbuilder/plancontext/planning_context_test.go +++ b/go/vt/vtgate/planbuilder/plancontext/planning_context_test.go @@ -197,7 +197,7 @@ func (v *vschema) FindTable(tablename sqlparser.TableName) (*vindexes.BaseTable, panic("implement me") } -func (v *vschema) FindView(name sqlparser.TableName) sqlparser.TableStatement { +func (v *vschema) FindView(name sqlparser.TableName) (sqlparser.TableStatement, *sqlparser.TableName) { // TODO implement me panic("implement me") } diff --git a/go/vt/vtgate/planbuilder/plancontext/vschema.go b/go/vt/vtgate/planbuilder/plancontext/vschema.go index 11751c014c4..860d34be476 100644 --- a/go/vt/vtgate/planbuilder/plancontext/vschema.go +++ b/go/vt/vtgate/planbuilder/plancontext/vschema.go @@ -25,7 +25,7 @@ type PlannerVersion = querypb.ExecuteOptions_PlannerVersion // info about tables. type VSchema interface { FindTable(tablename sqlparser.TableName) (*vindexes.BaseTable, string, topodatapb.TabletType, key.ShardDestination, error) - FindView(name sqlparser.TableName) sqlparser.TableStatement + FindView(name sqlparser.TableName) (sqlparser.TableStatement, *sqlparser.TableName) // FindViewTarget finds the target keyspace for the view table provided. FindViewTarget(name sqlparser.TableName) (*vindexes.Keyspace, error) FindTableOrVindex(tablename sqlparser.TableName) (*vindexes.BaseTable, vindexes.Vindex, string, topodatapb.TabletType, key.ShardDestination, error) diff --git a/go/vt/vtgate/vindexes/vschema.go b/go/vt/vtgate/vindexes/vschema.go index cf838567548..678b443e67f 100644 --- a/go/vt/vtgate/vindexes/vschema.go +++ b/go/vt/vtgate/vindexes/vschema.go @@ -65,6 +65,9 @@ type VSchema struct { MirrorRules map[string]*MirrorRule `json:"mirror_rules"` RoutingRules map[string]*RoutingRule `json:"routing_rules"` + // ViewRoutingRules stores routing rules for views. + ViewRoutingRules map[string]*ViewRoutingRule `json:"view_routing_rules"` + // globalTables contains the name of all tables in all keyspaces. If the // table is uniquely named, the value will be the qualified Table object // with the keyspace where this table exists. If multiple keyspaces have a @@ -119,6 +122,20 @@ func (rr *RoutingRule) MarshalJSON() ([]byte, error) { return json.Marshal(tables) } +// ViewRoutingRule represents a routing rule for a view. +type ViewRoutingRule struct { + // TargetKeyspace is the keyspace where the target view resides. + TargetKeyspace string + + // TargetViewName is the name of the view to route to. + TargetViewName string +} + +// MarshalJSON returns a JSON representation of ViewRoutingRule. +func (vrr *ViewRoutingRule) MarshalJSON() ([]byte, error) { + return json.Marshal(vrr.TargetKeyspace + "." + vrr.TargetViewName) +} + // View represents a view in VSchema. type View struct { Name string @@ -352,12 +369,13 @@ func (source *Source) String() string { // BuildVSchema builds a VSchema from a SrvVSchema. func BuildVSchema(source *vschemapb.SrvVSchema, parser *sqlparser.Parser) (vschema *VSchema) { vschema = &VSchema{ - MirrorRules: make(map[string]*MirrorRule), - RoutingRules: make(map[string]*RoutingRule), - globalTables: make(map[string]Table), - uniqueVindexes: make(map[string]Vindex), - Keyspaces: make(map[string]*KeyspaceSchema), - created: time.Now(), + MirrorRules: make(map[string]*MirrorRule), + RoutingRules: make(map[string]*RoutingRule), + ViewRoutingRules: make(map[string]*ViewRoutingRule), + globalTables: make(map[string]Table), + uniqueVindexes: make(map[string]Vindex), + Keyspaces: make(map[string]*KeyspaceSchema), + created: time.Now(), } buildKeyspaces(source, vschema, parser) // buildGlobalTables before buildReferences so that buildReferences can @@ -1069,6 +1087,20 @@ outer: } continue outer } + + // Check for views first. We do this first because FindTable may return a virtual table for + // unsharded keyspaces even if the table doesn't exist in the vschema. By checking for views + // first, we ensure that routing rules targeting views are correctly identified, rather than + // assuming the reference is a table. + if vschema.FindView(toKeyspace, toTableName) != nil { + vschema.ViewRoutingRules[rule.FromTable] = &ViewRoutingRule{ + TargetKeyspace: toKeyspace, + TargetViewName: toTableName, + } + + continue outer + } + t, err := vschema.FindTable(toKeyspace, toTableName) if err != nil { vschema.RoutingRules[rule.FromTable] = &RoutingRule{ @@ -1092,6 +1124,13 @@ func buildShardRoutingRule(source *vschemapb.SrvVSchema, vschema *VSchema) { } } +// RebuildRoutingRules rebuilds the routing rules. +func RebuildRoutingRules(source *vschemapb.SrvVSchema, vschema *VSchema, parser *sqlparser.Parser) { + vschema.RoutingRules = make(map[string]*RoutingRule) + vschema.ViewRoutingRules = make(map[string]*ViewRoutingRule) + buildRoutingRule(source, vschema, parser) +} + func buildKeyspaceRoutingRule(source *vschemapb.SrvVSchema, vschema *VSchema) { vschema.KeyspaceRoutingRules = nil sourceRules := source.GetKeyspaceRoutingRules().GetRules() @@ -1535,6 +1574,33 @@ func (vschema *VSchema) FindView(keyspace, name string) sqlparser.TableStatement }, nil).(sqlparser.TableStatement) } +// FindRoutedView finds a view, checking the view routing rules first. Returns the view's definition if found, +// along with the target view name if a routing rule matched. +func (vschema *VSchema) FindRoutedView(keyspace, viewName string, tabletType topodatapb.TabletType) (sqlparser.TableStatement, *sqlparser.TableName) { + // Apply keyspace routing rules first + keyspace = vschema.findRoutedKeyspace(keyspace, tabletType) + + // Build lookup keys + qualified := viewName + if keyspace != "" { + qualified = keyspace + "." + viewName + } + fqvn := qualified + TabletTypeSuffix[tabletType] + + // Check view routing rules + for _, name := range []string{fqvn, qualified, viewName + TabletTypeSuffix[tabletType], viewName} { + if rr, ok := vschema.ViewRoutingRules[name]; ok { + routedView := vschema.FindView(rr.TargetKeyspace, rr.TargetViewName) + routedViewName := sqlparser.NewTableNameWithQualifier(rr.TargetViewName, rr.TargetKeyspace) + + return routedView, &routedViewName + } + } + + // No routing rule matched, lookup view normally. + return vschema.FindView(keyspace, viewName), nil +} + // NotFoundError represents the error where the table name was not found type NotFoundError struct { TableName string diff --git a/go/vt/vtgate/vindexes/vschema_routing_test.go b/go/vt/vtgate/vindexes/vschema_routing_test.go index 2b0b1bf7efe..48741888627 100644 --- a/go/vt/vtgate/vindexes/vschema_routing_test.go +++ b/go/vt/vtgate/vindexes/vschema_routing_test.go @@ -466,6 +466,7 @@ func TestVSchemaRoutingRules(t *testing.T) { Error: errors.New("table t2 not found"), }, }, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "t1": t1, "t2": t2, diff --git a/go/vt/vtgate/vindexes/vschema_test.go b/go/vt/vtgate/vindexes/vschema_test.go index 2497ac660c5..c7e9fec1c64 100644 --- a/go/vt/vtgate/vindexes/vschema_test.go +++ b/go/vt/vtgate/vindexes/vschema_test.go @@ -978,7 +978,8 @@ func TestVSchemaMirrorRules(t *testing.T) { Error: errors.New("mirror chaining is not allowed"), }, }, - RoutingRules: map[string]*RoutingRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, Keyspaces: map[string]*KeyspaceSchema{ "ks1": { Keyspace: ks1, @@ -1372,8 +1373,9 @@ func TestShardedVSchemaMultiColumnVindex(t *testing.T) { t1.ColumnVindexes[0], } want := &VSchema{ - MirrorRules: map[string]*MirrorRule{}, - RoutingRules: map[string]*RoutingRule{}, + MirrorRules: map[string]*MirrorRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "t1": t1, }, @@ -1449,8 +1451,9 @@ func TestShardedVSchemaNotOwned(t *testing.T) { t1.ColumnVindexes[1], t1.ColumnVindexes[0]} want := &VSchema{ - MirrorRules: map[string]*MirrorRule{}, - RoutingRules: map[string]*RoutingRule{}, + MirrorRules: map[string]*MirrorRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "t1": t1, }, @@ -1557,8 +1560,9 @@ func TestBuildVSchemaDupSeq(t *testing.T) { Keyspace: ksb, Type: "sequence"} want := &VSchema{ - MirrorRules: map[string]*MirrorRule{}, - RoutingRules: map[string]*RoutingRule{}, + MirrorRules: map[string]*MirrorRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "t1": nil, }, @@ -1619,8 +1623,9 @@ func TestBuildVSchemaDupTable(t *testing.T) { Keyspace: ksb, } want := &VSchema{ - MirrorRules: map[string]*MirrorRule{}, - RoutingRules: map[string]*RoutingRule{}, + MirrorRules: map[string]*MirrorRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "t1": nil, }, @@ -1749,8 +1754,9 @@ func TestBuildVSchemaDupVindex(t *testing.T) { t2.ColumnVindexes[0], } want := &VSchema{ - MirrorRules: map[string]*MirrorRule{}, - RoutingRules: map[string]*RoutingRule{}, + MirrorRules: map[string]*MirrorRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "t1": nil, }, @@ -2336,8 +2342,9 @@ func TestSequence(t *testing.T) { t2.ColumnVindexes[0], } want := &VSchema{ - MirrorRules: map[string]*MirrorRule{}, - RoutingRules: map[string]*RoutingRule{}, + MirrorRules: map[string]*MirrorRule{}, + RoutingRules: map[string]*RoutingRule{}, + ViewRoutingRules: map[string]*ViewRoutingRule{}, globalTables: map[string]Table{ "seq": seq, "t1": t1, diff --git a/go/vt/vtgate/vschema_manager.go b/go/vt/vtgate/vschema_manager.go index 3e0e81aa476..fb938531c57 100644 --- a/go/vt/vtgate/vschema_manager.go +++ b/go/vt/vtgate/vschema_manager.go @@ -205,6 +205,9 @@ func (vm *VSchemaManager) buildAndEnhanceVSchema(v *vschemapb.SrvVSchema) *vinde // We need to skip if already present, to handle the case where MoveTables has switched traffic // and removed the source vschema but not from the source database because user asked to --keep-data vindexes.AddAdditionalGlobalTables(v, vschema) + + // Since views may have changed, we need to rebuild routing rules so that views are properly considered. + vindexes.RebuildRoutingRules(v, vschema, vm.parser) } return vschema } diff --git a/go/vt/vtgate/vschema_manager_test.go b/go/vt/vtgate/vschema_manager_test.go index 267f83f274d..7dbd388bdde 100644 --- a/go/vt/vtgate/vschema_manager_test.go +++ b/go/vt/vtgate/vschema_manager_test.go @@ -235,8 +235,9 @@ func TestVSchemaUpdate(t *testing.T) { }, }, expected: &vindexes.VSchema{ - MirrorRules: map[string]*vindexes.MirrorRule{}, - RoutingRules: map[string]*vindexes.RoutingRule{}, + MirrorRules: map[string]*vindexes.MirrorRule{}, + RoutingRules: map[string]*vindexes.RoutingRule{}, + ViewRoutingRules: map[string]*vindexes.ViewRoutingRule{}, Keyspaces: map[string]*vindexes.KeyspaceSchema{ "ks": { Keyspace: ks, @@ -501,8 +502,9 @@ func TestVSchemaUDFsUpdate(t *testing.T) { }, nil) utils.MustMatchFn(".globalTables", ".uniqueVindexes")(t, &vindexes.VSchema{ - MirrorRules: map[string]*vindexes.MirrorRule{}, - RoutingRules: map[string]*vindexes.RoutingRule{}, + MirrorRules: map[string]*vindexes.MirrorRule{}, + ViewRoutingRules: map[string]*vindexes.ViewRoutingRule{}, + RoutingRules: map[string]*vindexes.RoutingRule{}, Keyspaces: map[string]*vindexes.KeyspaceSchema{ "ks": { Keyspace: ks, @@ -655,7 +657,8 @@ func TestMarkErrorIfCyclesInFk(t *testing.T) { return vschema }, errWanted: "", - }, { + }, + { name: "Self-referencing foreign key with delete cascade", getVschema: func() *vindexes.VSchema { vschema := &vindexes.VSchema{ @@ -683,7 +686,8 @@ func TestMarkErrorIfCyclesInFk(t *testing.T) { return vschema }, errWanted: "VT09019: keyspace 'ks' has cyclic foreign keys. Cycle exists between [ks.t1.id ks.t1.id]", - }, { + }, + { name: "Self-referencing foreign key without delete cascade", getVschema: func() *vindexes.VSchema { vschema := &vindexes.VSchema{ @@ -711,7 +715,8 @@ func TestMarkErrorIfCyclesInFk(t *testing.T) { return vschema }, errWanted: "", - }, { + }, + { name: "Has an indirect cycle because of cascades", getVschema: func() *vindexes.VSchema { vschema := &vindexes.VSchema{ @@ -758,7 +763,8 @@ func TestMarkErrorIfCyclesInFk(t *testing.T) { return vschema }, errWanted: "VT09019: keyspace 'ks' has cyclic foreign keys", - }, { + }, + { name: "Cycle part of a multi-column foreign key", getVschema: func() *vindexes.VSchema { vschema := &vindexes.VSchema{ @@ -862,8 +868,9 @@ func TestVSchemaUpdateWithFKReferenceToInternalTables(t *testing.T) { }, nil) utils.MustMatchFn(".globalTables", ".uniqueVindexes")(t, &vindexes.VSchema{ - MirrorRules: map[string]*vindexes.MirrorRule{}, - RoutingRules: map[string]*vindexes.RoutingRule{}, + MirrorRules: map[string]*vindexes.MirrorRule{}, + RoutingRules: map[string]*vindexes.RoutingRule{}, + ViewRoutingRules: map[string]*vindexes.ViewRoutingRule{}, Keyspaces: map[string]*vindexes.KeyspaceSchema{ "ks": { Keyspace: ks, @@ -976,9 +983,10 @@ func makeTestVSchema(ks string, sharded bool, tbls map[string]*vindexes.BaseTabl func makeTestEmptyVSchema() *vindexes.VSchema { return &vindexes.VSchema{ - MirrorRules: map[string]*vindexes.MirrorRule{}, - RoutingRules: map[string]*vindexes.RoutingRule{}, - Keyspaces: map[string]*vindexes.KeyspaceSchema{}, + MirrorRules: map[string]*vindexes.MirrorRule{}, + RoutingRules: map[string]*vindexes.RoutingRule{}, + ViewRoutingRules: map[string]*vindexes.ViewRoutingRule{}, + Keyspaces: map[string]*vindexes.KeyspaceSchema{}, } } @@ -994,6 +1002,88 @@ func makeTestSrvVSchema(ks string, sharded bool, tbls map[string]*vschemapb.Tabl } } +// TestViewRoutingRules tests that routing rules targeting views are created as view routing rules. +func TestViewRoutingRules(t *testing.T) { + vm := &VSchemaManager{} + var vs *vindexes.VSchema + vm.subscriber = func(vschema *vindexes.VSchema, _ *VSchemaStats) { + vs = vschema + vs.ResetCreated() + } + vm.schema = &fakeSchema{ + views: map[string]map[string]sqlparser.TableStatement{ + "source_ks": {"v1": testView("t1")}, + "target_ks": {"v1": testView("t2")}, + }, + } + + vm.VSchemaUpdate(&vschemapb.SrvVSchema{ + Keyspaces: map[string]*vschemapb.Keyspace{ + "source_ks": {}, + "target_ks": {}, + }, + RoutingRules: &vschemapb.RoutingRules{ + Rules: []*vschemapb.RoutingRule{ + {FromTable: "source_ks.v1", ToTables: []string{"target_ks.v1"}}, + }, + }, + }, nil) + + require.NotNil(t, vs) + require.Contains(t, vs.ViewRoutingRules, "source_ks.v1") + assert.Equal(t, "target_ks", vs.ViewRoutingRules["source_ks.v1"].TargetKeyspace) + assert.Equal(t, "v1", vs.ViewRoutingRules["source_ks.v1"].TargetViewName) +} + +// TestViewRoutingRulesRebuild tests that view routing rules are correctly created when views +// are added after the initial vschema is built. +func TestViewRoutingRulesRebuild(t *testing.T) { + vm := &VSchemaManager{} + var vs *vindexes.VSchema + vm.subscriber = func(vschema *vindexes.VSchema, _ *VSchemaStats) { + vs = vschema + vs.ResetCreated() + } + + srvVSchema := &vschemapb.SrvVSchema{ + Keyspaces: map[string]*vschemapb.Keyspace{ + "source_ks": {}, + "target_ks": {}, + }, + RoutingRules: &vschemapb.RoutingRules{ + Rules: []*vschemapb.RoutingRule{ + {FromTable: "source_ks.v1", ToTables: []string{"target_ks.v1"}}, + }, + }, + } + + fs := &fakeSchema{} + vm.schema = fs + + vm.VSchemaUpdate(srvVSchema, nil) + require.NotNil(t, vs) + assert.NotContains(t, vs.ViewRoutingRules, "source_ks.v1") + + fs.views = map[string]map[string]sqlparser.TableStatement{ + "source_ks": {"v1": testView("t1")}, + "target_ks": {"v1": testView("t2")}, + } + + vm.Rebuild() + + require.Contains(t, vs.ViewRoutingRules, "source_ks.v1") + assert.Equal(t, "target_ks", vs.ViewRoutingRules["source_ks.v1"].TargetKeyspace) + assert.Equal(t, "v1", vs.ViewRoutingRules["source_ks.v1"].TargetViewName) +} + +// testView creates a simple view selecting from the given table. +func testView(tableName string) *sqlparser.Select { + return &sqlparser.Select{ + SelectExprs: &sqlparser.SelectExprs{Exprs: []sqlparser.SelectExpr{sqlparser.NewAliasedExpr(sqlparser.NewIntLiteral("1"), "")}}, + From: []sqlparser.TableExpr{sqlparser.NewAliasedTableExpr(sqlparser.NewTableName(tableName), "")}, + } +} + type fakeSchema struct { // Single keyspace (backward compatibility) t map[string]*vindexes.TableInfo From 7b3d92433664bc7c92b0f5803bbf3e94e480fdb1 Mon Sep 17 00:00:00 2001 From: Mohamed Hamza Date: Wed, 7 Jan 2026 18:11:04 -0500 Subject: [PATCH 2/3] fix test Signed-off-by: Mohamed Hamza --- go/vt/vttablet/tabletserver/vstreamer/engine_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/vt/vttablet/tabletserver/vstreamer/engine_test.go b/go/vt/vttablet/tabletserver/vstreamer/engine_test.go index 7cafcc6d485..a510f00ff21 100644 --- a/go/vt/vttablet/tabletserver/vstreamer/engine_test.go +++ b/go/vt/vttablet/tabletserver/vstreamer/engine_test.go @@ -108,6 +108,7 @@ func TestUpdateVSchema(t *testing.T) { want := `{ "mirror_rules": {}, "routing_rules": {}, + "view_routing_rules": {}, "keyspaces": { "vttest": { "sharded": true, From ef0a54b06d853a528119d7a1e64785a196d5554c Mon Sep 17 00:00:00 2001 From: Mohamed Hamza Date: Mon, 12 Jan 2026 15:08:36 -0500 Subject: [PATCH 3/3] add changelog entry Signed-off-by: Mohamed Hamza --- changelog/24.0/24.0.0/summary.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/changelog/24.0/24.0.0/summary.md b/changelog/24.0/24.0.0/summary.md index bf7a89b9959..4f358330079 100644 --- a/changelog/24.0/24.0.0/summary.md +++ b/changelog/24.0/24.0.0/summary.md @@ -6,6 +6,7 @@ - **[Major Changes](#major-changes)** - **[New Support](#new-support)** - [Window function pushdown for sharded keyspaces](#window-function-pushdown) + - [View Routing Rules](#view-routing-rules) - **[Minor Changes](#minor-changes)** - **[VTGate](#minor-changes-vtgate)** - [New default for `--legacy-replication-lag-algorithm` flag](#vtgate-new-default-legacy-replication-lag-algorithm) @@ -34,6 +35,36 @@ Previously, all window function queries required single-shard routing, which lim For examples and more details, see the [documentation](https://vitess.io/docs/24.0/reference/compatibility/mysql-compatibility/#window-functions). +### View Routing Rules + +Vitess now supports routing rules for views, and can be applied the same as tables with `vtctldclient ApplyRoutingRules`. When a view routing rule is active, VTGate rewrites queries that reference the source view to use the target view's definition instead. For example, given this routing rule: + +```json +{ + "rules": [ + { + "from_table": "source_ks.my_view", + "to_tables": ["target_ks.my_view"] + } + ] +} +``` + +And this view definition: + +```sql +CREATE VIEW target_ks.my_view AS SELECT id, name FROM user; +``` + +A query like `SELECT * FROM source_ks.my_view` would be internally rewritten to: + +```sql +SELECT * FROM (SELECT id, name FROM target_ks.user) AS my_view; +``` + +View routing rules require the schema tracker to monitor views, which means VTGate must be started with the `--enable-views` flag and VTTablet with the `--queryserver-enable-views` flag. The target view must exist in the specified keyspace for the routing rule to function correctly. For more details, see the [Schema Routing Rules documentation](https://vitess.io/docs/24.0/reference/features/schema-routing-rules/). + + ## Minor Changes ### VTGate