diff --git a/executor/executor_test.go b/executor/executor_test.go index 6dbc1c776ac39..42d67a5c39623 100644 --- a/executor/executor_test.go +++ b/executor/executor_test.go @@ -3427,6 +3427,41 @@ func (s *testSuite3) TestSelectHashPartitionTable(c *C) { )) } +func (s *testSuite) TestSelectView(c *C) { + tk := testkit.NewTestKit(c, s.store) + tk.MustExec("use test") + tk.MustExec("create table view_t (a int,b int)") + tk.MustExec("insert into view_t values(1,2)") + tk.MustExec("create view view1 as select * from view_t") + tk.MustExec("create view view2(c,d) as select * from view_t") + tk.MustExec("create view view3(c,d) as select a,b from view_t") + tk.MustQuery("select * from view1;").Check(testkit.Rows("1 2")) + tk.MustQuery("select * from view2;").Check(testkit.Rows("1 2")) + tk.MustQuery("select * from view3;").Check(testkit.Rows("1 2")) + tk.MustExec("drop table view_t;") + tk.MustExec("create table view_t(c int,d int)") + _, err := tk.Exec("select * from view1") + c.Assert(err.Error(), Equals, plannercore.ErrViewInvalid.GenWithStackByArgs("test", "view1").Error()) + _, err = tk.Exec("select * from view2") + c.Assert(err.Error(), Equals, plannercore.ErrViewInvalid.GenWithStackByArgs("test", "view2").Error()) + _, err = tk.Exec("select * from view3") + c.Assert(err.Error(), Equals, "[planner:1054]Unknown column 'a' in 'field list'") + tk.MustExec("drop table view_t;") + tk.MustExec("create table view_t(a int,b int,c int)") + tk.MustExec("insert into view_t values(1,2,3)") + tk.MustQuery("select * from view1;").Check(testkit.Rows("1 2")) + tk.MustQuery("select * from view2;").Check(testkit.Rows("1 2")) + tk.MustQuery("select * from view3;").Check(testkit.Rows("1 2")) + tk.MustExec("alter table view_t drop column a") + tk.MustExec("alter table view_t add column a int after b") + tk.MustExec("update view_t set a=1;") + tk.MustQuery("select * from view1;").Check(testkit.Rows("1 2")) + tk.MustQuery("select * from view2;").Check(testkit.Rows("1 2")) + tk.MustQuery("select * from view3;").Check(testkit.Rows("1 2")) + tk.MustExec("drop table view_t;") + tk.MustExec("drop view view1,view2,view3;") +} + type testSuite2 struct { cluster *mocktikv.Cluster mvccStore mocktikv.MVCCStore diff --git a/planner/core/errors.go b/planner/core/errors.go index 48645a062988a..6d029c490f533 100644 --- a/planner/core/errors.go +++ b/planner/core/errors.go @@ -87,6 +87,7 @@ var ( ErrMixOfGroupFuncAndFields = terror.ClassOptimizer.New(codeMixOfGroupFuncAndFields, "In aggregated query without GROUP BY, expression #%d of SELECT list contains nonaggregated column '%s'; this is incompatible with sql_mode=only_full_group_by") ErrNonUniqTable = terror.ClassOptimizer.New(codeNonUniqTable, mysql.MySQLErrName[mysql.ErrNonuniqTable]) ErrWrongValueCountOnRow = terror.ClassOptimizer.New(mysql.ErrWrongValueCountOnRow, mysql.MySQLErrName[mysql.ErrWrongValueCountOnRow]) + ErrViewInvalid = terror.ClassOptimizer.New(mysql.ErrViewInvalid, mysql.MySQLErrName[mysql.ErrViewInvalid]) ) func init() { diff --git a/planner/core/logical_plan_builder.go b/planner/core/logical_plan_builder.go index 073b617da5524..72aa76dddb3e4 100644 --- a/planner/core/logical_plan_builder.go +++ b/planner/core/logical_plan_builder.go @@ -1902,6 +1902,10 @@ func (b *PlanBuilder) buildDataSource(tn *ast.TableName) (LogicalPlan, error) { tableInfo := tbl.Meta() b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, dbName.L, tableInfo.Name.L, "") + if tableInfo.IsView() { + return b.buildDataSourceFromView(dbName, tableInfo) + } + if tableInfo.GetPartitionInfo() != nil { b.optFlag = b.optFlag | flagPartitionProcessor } @@ -1994,6 +1998,42 @@ func (b *PlanBuilder) buildDataSource(tn *ast.TableName) (LogicalPlan, error) { return result, nil } +func (b *PlanBuilder) buildDataSourceFromView(dbName model.CIStr, tableInfo *model.TableInfo) (LogicalPlan, error) { + charset, collation := b.ctx.GetSessionVars().GetCharsetInfo() + selectNode, err := parser.New().ParseOneStmt(tableInfo.View.SelectStmt, charset, collation) + if err != nil { + return nil, err + } + selectLogicalPlan, err := b.Build(selectNode) + if err != nil { + return nil, err + } + + projSchema := expression.NewSchema(make([]*expression.Column, 0, len(tableInfo.View.Cols))...) + projExprs := make([]expression.Expression, 0, len(tableInfo.View.Cols)) + for i := range tableInfo.View.Cols { + col := selectLogicalPlan.Schema().FindColumnByName(tableInfo.View.Cols[i].L) + if col == nil { + return nil, ErrViewInvalid.GenWithStackByArgs(dbName.O, tableInfo.Name.O) + } + projSchema.Append(&expression.Column{ + UniqueID: b.ctx.GetSessionVars().AllocPlanColumnID(), + TblName: col.TblName, + OrigTblName: col.OrigTblName, + ColName: tableInfo.Cols()[i].Name, + OrigColName: tableInfo.View.Cols[i], + DBName: col.DBName, + RetType: col.GetType(), + }) + projExprs = append(projExprs, col) + } + + projUponView := LogicalProjection{Exprs: projExprs}.Init(b.ctx) + projUponView.SetChildren(selectLogicalPlan.(LogicalPlan)) + projUponView.SetSchema(projSchema) + return projUponView, nil +} + // projectVirtualColumns is only for DataSource. If some table has virtual generated columns, // we add a projection on the original DataSource, and calculate those columns in the projection // so that plans above it can reference generated columns by their name. diff --git a/planner/core/logical_plan_test.go b/planner/core/logical_plan_test.go index f9a3497fbae12..24c7cfc0e1b85 100644 --- a/planner/core/logical_plan_test.go +++ b/planner/core/logical_plan_test.go @@ -45,7 +45,7 @@ type testPlanSuite struct { } func (s *testPlanSuite) SetUpSuite(c *C) { - s.is = infoschema.MockInfoSchema([]*model.TableInfo{MockTable()}) + s.is = infoschema.MockInfoSchema([]*model.TableInfo{MockTable(), MockView()}) s.ctx = MockContext() s.Parser = parser.New() } @@ -1873,3 +1873,34 @@ func (s *testPlanSuite) TestOuterJoinEliminator(c *C) { c.Assert(ToString(p), Equals, tt.best, comment) } } + +func (s *testPlanSuite) TestSelectView(c *C) { + defer func() { + testleak.AfterTest(c)() + }() + tests := []struct { + sql string + best string + }{ + { + sql: "select * from v", + best: "DataScan(t)->Projection", + }, + } + for i, tt := range tests { + comment := Commentf("case:%v sql:%s", i, tt.sql) + stmt, err := s.ParseOneStmt(tt.sql, "", "") + c.Assert(err, IsNil, comment) + Preprocess(s.ctx, stmt, s.is, false) + builder := &PlanBuilder{ + ctx: MockContext(), + is: s.is, + colMapper: make(map[*ast.ColumnNameExpr]int), + } + p, err := builder.Build(stmt) + c.Assert(err, IsNil) + p, err = logicalOptimize(builder.optFlag, p.(LogicalPlan)) + c.Assert(err, IsNil) + c.Assert(ToString(p), Equals, tt.best, comment) + } +} diff --git a/planner/core/mock.go b/planner/core/mock.go index 316a4c389bb92..4b4a6a12e54da 100644 --- a/planner/core/mock.go +++ b/planner/core/mock.go @@ -14,6 +14,7 @@ package core import ( + "github.com/pingcap/parser/auth" "github.com/pingcap/parser/model" "github.com/pingcap/parser/mysql" "github.com/pingcap/tidb/domain" @@ -249,6 +250,36 @@ func MockTable() *model.TableInfo { return table } +// MockView is only used for plan related tests. +func MockView() *model.TableInfo { + selectStmt := "select b,c,d from t" + col0 := &model.ColumnInfo{ + State: model.StatePublic, + Offset: 0, + Name: model.NewCIStr("b"), + ID: 1, + } + col1 := &model.ColumnInfo{ + State: model.StatePublic, + Offset: 1, + Name: model.NewCIStr("c"), + ID: 2, + } + col2 := &model.ColumnInfo{ + State: model.StatePublic, + Offset: 2, + Name: model.NewCIStr("d"), + ID: 3, + } + view := &model.ViewInfo{SelectStmt: selectStmt, Security: model.SecurityDefiner, Definer: &auth.UserIdentity{Username: "root", Hostname: ""}, Cols: []model.CIStr{col0.Name, col1.Name, col2.Name}} + table := &model.TableInfo{ + Name: model.NewCIStr("v"), + Columns: []*model.ColumnInfo{col0, col1, col2}, + View: view, + } + return table +} + // MockContext is only used for plan related tests. func MockContext() sessionctx.Context { ctx := mock.NewContext()