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
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
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,