Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions changelog/24.0/24.0.0/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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).

### <a id="view-routing-rules"/>View Routing Rules</a>

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/).


## <a id="minor-changes"/>Minor Changes</a>

### <a id="minor-changes-vtgate"/>VTGate</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sharded": false,
"tables": {
"t1": {}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"sharded": true,
"vindexes": {
"xxhash": {
"type": "xxhash"
}
},
"tables": {
"t1": {
"column_vindexes": [
{
"column": "id",
"name": "xxhash"
}
]
}
}
}
169 changes: 169 additions & 0 deletions go/test/endtoend/vtgate/schematracker/view_routing_rules/vrr_test.go
Original file line number Diff line number Diff line change
@@ -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")]]`)
}
6 changes: 3 additions & 3 deletions go/test/vschemawrapper/vschema_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions go/vt/sqlparser/normalizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
)

Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 4 additions & 4 deletions go/vt/sqlparser/normalizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions go/vt/vtgate/executorcontext/vcursor_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
2 changes: 1 addition & 1 deletion go/vt/vtgate/planbuilder/plancontext/vschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading