diff --git a/executor/infoschema_reader.go b/executor/infoschema_reader.go index 0ad44e959465f..a0f01aa668653 100644 --- a/executor/infoschema_reader.go +++ b/executor/infoschema_reader.go @@ -57,6 +57,7 @@ import ( "github.com/pingcap/tidb/util/chunk" "github.com/pingcap/tidb/util/collate" "github.com/pingcap/tidb/util/deadlockhistory" + "github.com/pingcap/tidb/util/hint" "github.com/pingcap/tidb/util/keydecoder" "github.com/pingcap/tidb/util/logutil" "github.com/pingcap/tidb/util/pdapi" @@ -615,85 +616,119 @@ func (e *hugeMemTableRetriever) dataForColumnsInTable(ctx context.Context, sctx sctx.GetSessionVars().StmtCtx.AppendWarning(err) return } + is := sctx.GetInfoSchema().(infoschema.InfoSchema) + var viewSchema *expression.Schema + var viewOutputNames types.NameSlice + if tbl.IsView() { + var viewLogicalPlan plannercore.Plan + // Build plan is not thread safe, there will be concurrency on sessionctx. + if err := runWithSystemSession(sctx, func(s sessionctx.Context) error { + planBuilder, _ := plannercore.NewPlanBuilder().Init(s, is, &hint.BlockHintProcessor{}) + var err error + viewLogicalPlan, err = planBuilder.BuildDataSourceFromView(ctx, schema.Name, tbl) + if err != nil { + return errors.Trace(err) + } + return nil + }); err != nil { + sctx.GetSessionVars().StmtCtx.AppendWarning(err) + return + } + + viewSchema = viewLogicalPlan.Schema() + viewOutputNames = viewLogicalPlan.OutputNames() + } + for i, col := range tbl.Columns { if col.Hidden { continue } + + ft := &col.FieldType + if viewSchema != nil { + // If this is a view, replace the column with the view column. + idx := expression.FindFieldNameIdxByColName(viewOutputNames, col.Name.L) + if idx >= 0 { + col1 := viewSchema.Columns[idx] + ft = col1.GetType() + } + } + var charMaxLen, charOctLen, numericPrecision, numericScale, datetimePrecision interface{} - colLen, decimal := col.Flen, col.Decimal - defaultFlen, defaultDecimal := mysql.GetDefaultFieldLengthAndDecimal(col.Tp) + colLen, decimal := ft.Flen, ft.Decimal + defaultFlen, defaultDecimal := mysql.GetDefaultFieldLengthAndDecimal(ft.Tp) if decimal == types.UnspecifiedLength { decimal = defaultDecimal } if colLen == types.UnspecifiedLength { colLen = defaultFlen } - if col.Tp == mysql.TypeSet { + if ft.Tp == mysql.TypeSet { // Example: In MySQL set('a','bc','def','ghij') has length 13, because // len('a')+len('bc')+len('def')+len('ghij')+len(ThreeComma)=13 // Reference link: https://bugs.mysql.com/bug.php?id=22613 colLen = 0 - for _, ele := range col.Elems { + for _, ele := range ft.Elems { colLen += len(ele) } - if len(col.Elems) != 0 { - colLen += (len(col.Elems) - 1) + if len(ft.Elems) != 0 { + colLen += (len(ft.Elems) - 1) } charMaxLen = colLen - charOctLen = calcCharOctLength(colLen, col.Charset) - } else if col.Tp == mysql.TypeEnum { + charOctLen = calcCharOctLength(colLen, ft.Charset) + } else if ft.Tp == mysql.TypeEnum { // Example: In MySQL enum('a', 'ab', 'cdef') has length 4, because // the longest string in the enum is 'cdef' // Reference link: https://bugs.mysql.com/bug.php?id=22613 colLen = 0 - for _, ele := range col.Elems { + for _, ele := range ft.Elems { if len(ele) > colLen { colLen = len(ele) } } charMaxLen = colLen - charOctLen = calcCharOctLength(colLen, col.Charset) - } else if types.IsString(col.Tp) { + charOctLen = calcCharOctLength(colLen, ft.Charset) + } else if types.IsString(ft.Tp) { charMaxLen = colLen - charOctLen = calcCharOctLength(colLen, col.Charset) - } else if types.IsTypeFractionable(col.Tp) { + charOctLen = calcCharOctLength(colLen, ft.Charset) + } else if types.IsTypeFractionable(ft.Tp) { datetimePrecision = decimal - } else if types.IsTypeNumeric(col.Tp) { + } else if types.IsTypeNumeric(ft.Tp) { numericPrecision = colLen - if col.Tp != mysql.TypeFloat && col.Tp != mysql.TypeDouble { + if ft.Tp != mysql.TypeFloat && ft.Tp != mysql.TypeDouble { numericScale = decimal } else if decimal != -1 { numericScale = decimal } } - columnType := col.FieldType.InfoSchemaStr() + columnType := ft.InfoSchemaStr() columnDesc := table.NewColDesc(table.ToColumn(col)) var columnDefault interface{} if columnDesc.DefaultValue != nil { columnDefault = fmt.Sprintf("%v", columnDesc.DefaultValue) } record := types.MakeDatums( - infoschema.CatalogVal, // TABLE_CATALOG - schema.Name.O, // TABLE_SCHEMA - tbl.Name.O, // TABLE_NAME - col.Name.O, // COLUMN_NAME - i+1, // ORIGINAL_POSITION - columnDefault, // COLUMN_DEFAULT - columnDesc.Null, // IS_NULLABLE - types.TypeToStr(col.Tp, col.Charset), // DATA_TYPE - charMaxLen, // CHARACTER_MAXIMUM_LENGTH - charOctLen, // CHARACTER_OCTET_LENGTH - numericPrecision, // NUMERIC_PRECISION - numericScale, // NUMERIC_SCALE - datetimePrecision, // DATETIME_PRECISION - columnDesc.Charset, // CHARACTER_SET_NAME - columnDesc.Collation, // COLLATION_NAME - columnType, // COLUMN_TYPE - columnDesc.Key, // COLUMN_KEY - columnDesc.Extra, // EXTRA - "select,insert,update,references", // PRIVILEGES - columnDesc.Comment, // COLUMN_COMMENT - col.GeneratedExprString, // GENERATION_EXPRESSION + infoschema.CatalogVal, // TABLE_CATALOG + schema.Name.O, // TABLE_SCHEMA + tbl.Name.O, // TABLE_NAME + col.Name.O, // COLUMN_NAME + i+1, // ORIGINAL_POSITION + columnDefault, // COLUMN_DEFAULT + columnDesc.Null, // IS_NULLABLE + types.TypeToStr(ft.Tp, ft.Charset), // DATA_TYPE + charMaxLen, // CHARACTER_MAXIMUM_LENGTH + charOctLen, // CHARACTER_OCTET_LENGTH + numericPrecision, // NUMERIC_PRECISION + numericScale, // NUMERIC_SCALE + datetimePrecision, // DATETIME_PRECISION + columnDesc.Charset, // CHARACTER_SET_NAME + columnDesc.Collation, // COLLATION_NAME + columnType, // COLUMN_TYPE + columnDesc.Key, // COLUMN_KEY + columnDesc.Extra, // EXTRA + "select,insert,update,references", // PRIVILEGES + columnDesc.Comment, // COLUMN_COMMENT + col.GeneratedExprString, // GENERATION_EXPRESSION ) e.rows = append(e.rows, record) } diff --git a/executor/infoschema_reader_test.go b/executor/infoschema_reader_test.go index 400ca848a85c3..efa49445e02ba 100644 --- a/executor/infoschema_reader_test.go +++ b/executor/infoschema_reader_test.go @@ -981,3 +981,59 @@ func (s *testInfoschemaTableSuite) TestTablesPKType(c *C) { tk.MustQuery("SELECT TIDB_PK_TYPE FROM information_schema.tables where table_schema = 'test' and table_name = 't_common'").Check(testkit.Rows("CLUSTERED")) tk.MustQuery("SELECT TIDB_PK_TYPE FROM information_schema.tables where table_schema = 'INFORMATION_SCHEMA' and table_name = 'TABLES'").Check(testkit.Rows("NONCLUSTERED")) } + +// https://github.com/pingcap/tidb/issues/32459. +func (s *testInfoschemaTableSuite) TestJoinSystemTableContainsView(c *C) { + tk := testkit.NewTestKitWithInit(c, s.store) + tk.MustExec("use test") + tk.MustExec("create table t (a timestamp, b int);") + tk.MustExec("insert into t values (null, 100);") + tk.MustExec("create view v as select * from t;") + // This is used by grafana when TiDB is specified as the data source. + // See https://github.com/grafana/grafana/blob/e86b6662a187c77656f72bef3b0022bf5ced8b98/public/app/plugins/datasource/mysql/meta_query.ts#L31 + for i := 0; i < 10; i++ { + tk.MustQuery(` +SELECT + table_name as table_name, + ( SELECT + column_name as column_name + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN ('timestamp', 'datetime') + ORDER BY ordinal_position LIMIT 1 + ) AS time_column, + ( SELECT + column_name AS column_name + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN('float', 'int', 'bigint') + ORDER BY ordinal_position LIMIT 1 + ) AS value_column + FROM information_schema.tables t + WHERE + t.table_schema = database() AND + EXISTS + ( SELECT 1 + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN ('timestamp', 'datetime') + ) AND + EXISTS + ( SELECT 1 + FROM information_schema.columns c + WHERE + c.table_schema = t.table_schema AND + c.table_name = t.table_name AND + c.data_type IN('float', 'int', 'bigint') + ) + LIMIT 1 +; +`).Check(testkit.Rows("t a b")) + } +} diff --git a/executor/show.go b/executor/show.go index 3269321888222..c6c532075f79e 100644 --- a/executor/show.go +++ b/executor/show.go @@ -1761,9 +1761,15 @@ func (e *ShowExec) fetchShowBuiltins() error { // Because view's underlying table's column could change or recreate, so view's column type may change over time. // To avoid this situation we need to generate a logical plan and extract current column types from Schema. func tryFillViewColumnType(ctx context.Context, sctx sessionctx.Context, is infoschema.InfoSchema, dbName model.CIStr, tbl *model.TableInfo) error { - if tbl.IsView() { + if !tbl.IsView() { + return nil + } + // We need to run the build plan process in another session because there may be + // multiple goroutines running at the same time while session is not goroutine-safe. + // Take joining system table as an example, `fetchBuildSideRows` and `fetchProbeSideChunks` can be run concurrently. + return runWithSystemSession(sctx, func(s sessionctx.Context) error { // Retrieve view columns info. - planBuilder, _ := plannercore.NewPlanBuilder().Init(sctx, is, &hint.BlockHintProcessor{}) + planBuilder, _ := plannercore.NewPlanBuilder().Init(s, is, &hint.BlockHintProcessor{}) if viewLogicalPlan, err := planBuilder.BuildDataSourceFromView(ctx, dbName, tbl); err == nil { viewSchema := viewLogicalPlan.Schema() viewOutputNames := viewLogicalPlan.OutputNames() @@ -1779,6 +1785,23 @@ func tryFillViewColumnType(ctx context.Context, sctx sessionctx.Context, is info } else { return err } + return nil + }) +} + +func runWithSystemSession(sctx sessionctx.Context, fn func(sessionctx.Context) error) error { + b := &baseExecutor{ctx: sctx} + sysCtx, err := b.getSysSession() + if err != nil { + return err } - return nil + // TODO(tangenta): remove the CurrentDB assignment after + // https://github.com/pingcap/tidb/issues/34090 is fixed. + originDB := sysCtx.GetSessionVars().CurrentDB + sysCtx.GetSessionVars().CurrentDB = sctx.GetSessionVars().CurrentDB + defer func() { + sysCtx.GetSessionVars().CurrentDB = originDB + }() + defer b.releaseSysSession(sysCtx) + return fn(sysCtx) } diff --git a/infoschema/cluster_tables_test.go b/infoschema/cluster_tables_test.go new file mode 100644 index 0000000000000..872af6a11d78e --- /dev/null +++ b/infoschema/cluster_tables_test.go @@ -0,0 +1,71 @@ +// Copyright 2021 PingCAP, Inc. +// +// 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 infoschema_test + +import ( + "fmt" + "strconv" + "sync" + "testing" + + "github.com/pingcap/parser/auth" + "github.com/pingcap/tidb/domain" + "github.com/pingcap/tidb/kv" + "github.com/pingcap/tidb/testkit" + "github.com/stretchr/testify/require" +) + +type clusterTablesSuite struct { + store kv.Storage + dom *domain.Domain +} + +func TestStmtSummaryIssue35340(t *testing.T) { + s := new(clusterTablesSuite) + s.store, s.dom = testkit.CreateMockStoreAndDomain(t) + + tk := s.newTestKitWithRoot(t) + tk.MustExec("set global tidb_stmt_summary_refresh_interval=1800") + tk.MustExec("set global tidb_stmt_summary_max_stmt_count = 3000") + for i := 0; i < 100; i++ { + user := "user" + strconv.Itoa(i) + tk.MustExec(fmt.Sprintf("create user '%v'@'localhost'", user)) + } + tk.MustExec("flush privileges") + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + tk := s.newTestKitWithRoot(t) + for j := 0; j < 100; j++ { + user := "user" + strconv.Itoa(j) + require.True(t, tk.Session().Auth(&auth.UserIdentity{ + Username: user, + Hostname: "localhost", + }, nil, nil)) + tk.MustQuery("select count(*) from information_schema.statements_summary;") + } + }() + } + wg.Wait() +} + +func (s *clusterTablesSuite) newTestKitWithRoot(t *testing.T) *testkit.TestKit { + tk := testkit.NewTestKit(t, s.store) + tk.MustExec("use test") + require.True(t, tk.Session().Auth(&auth.UserIdentity{Username: "root", Hostname: "%"}, nil, nil)) + return tk +} diff --git a/testkit/mockstore.go b/testkit/mockstore.go index 7bad3b1caeb3b..e6ef207c985f0 100644 --- a/testkit/mockstore.go +++ b/testkit/mockstore.go @@ -11,13 +11,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !codes // +build !codes package testkit import ( "testing" + "time" + "github.com/pingcap/tidb/domain" "github.com/pingcap/tidb/kv" "github.com/pingcap/tidb/session" "github.com/pingcap/tidb/store/mockstore" @@ -44,3 +47,26 @@ func CreateMockStore(t *testing.T) (store kv.Storage, clean func()) { return } + +// CreateMockStoreAndDomain return a new mock kv.Storage and *domain.Domain. +func CreateMockStoreAndDomain(t testing.TB, opts ...mockstore.MockTiKVStoreOption) (kv.Storage, *domain.Domain) { + store, err := mockstore.NewMockStore(opts...) + require.NoError(t, err) + return store, bootstrap(t, store, 0) +} + +func bootstrap(t testing.TB, store kv.Storage, lease time.Duration) *domain.Domain { + session.SetSchemaLease(lease) + session.DisableStats4Test() + dom, err := session.BootstrapSession(store) + require.NoError(t, err) + + dom.SetStatsUpdating(true) + + t.Cleanup(func() { + dom.Close() + err := store.Close() + require.NoError(t, err) + }) + return dom +} diff --git a/util/stmtsummary/reader.go b/util/stmtsummary/reader.go index 0296344f99c14..3f8036f1e0a75 100644 --- a/util/stmtsummary/reader.go +++ b/util/stmtsummary/reader.go @@ -118,11 +118,7 @@ func (ssr *stmtSummaryReader) getStmtByDigestRow(ssbd *stmtSummaryByDigest, begi // `ssElement` is lazy expired, so expired elements could also be read. // `beginTime` won't change since `ssElement` is created, so locking is not needed here. - isAuthed := true - if ssr.user != nil && !ssr.isSuper { - _, isAuthed = ssElement.authUsers[ssr.user.Username] - } - if ssElement == nil || ssElement.beginTime < beginTimeForCurInterval || !isAuthed { + if ssElement == nil || ssElement.beginTime < beginTimeForCurInterval { return nil } return ssr.getStmtByDigestElementRow(ssElement, ssbd) @@ -131,6 +127,14 @@ func (ssr *stmtSummaryReader) getStmtByDigestRow(ssbd *stmtSummaryByDigest, begi func (ssr *stmtSummaryReader) getStmtByDigestElementRow(ssElement *stmtSummaryByDigestElement, ssbd *stmtSummaryByDigest) []types.Datum { ssElement.Lock() defer ssElement.Unlock() + isAuthed := true + if ssr.user != nil && !ssr.isSuper { + _, isAuthed = ssElement.authUsers[ssr.user.Username] + } + if !isAuthed { + return nil + } + datums := make([]types.Datum, len(ssr.columnValueFactories)) for i, factory := range ssr.columnValueFactories { datums[i] = types.NewDatum(factory(ssElement, ssbd)) @@ -144,12 +148,9 @@ func (ssr *stmtSummaryReader) getStmtByDigestHistoryRow(ssbd *stmtSummaryByDiges rows := make([][]types.Datum, 0, len(ssElements)) for _, ssElement := range ssElements { - isAuthed := true - if ssr.user != nil && !ssr.isSuper { - _, isAuthed = ssElement.authUsers[ssr.user.Username] - } - if isAuthed { - rows = append(rows, ssr.getStmtByDigestElementRow(ssElement, ssbd)) + record := ssr.getStmtByDigestElementRow(ssElement, ssbd) + if record != nil { + rows = append(rows, record) } } return rows @@ -180,7 +181,10 @@ func (ssr *stmtSummaryReader) getStmtEvictedOtherHistoryRow(ssbde *stmtSummaryBy ssbd := new(stmtSummaryByDigest) for _, seElement := range seElements { - rows = append(rows, ssr.getStmtByDigestElementRow(seElement.otherSummary, ssbd)) + record := ssr.getStmtByDigestElementRow(seElement.otherSummary, ssbd) + if record != nil { + rows = append(rows, record) + } } return rows }