From a221ae0c44c12f5f2f3dca9b2d658ae5786c20b6 Mon Sep 17 00:00:00 2001 From: crazycs Date: Tue, 23 Oct 2018 17:55:58 +0800 Subject: [PATCH 1/5] admin: refine admin check decoder (#7862) --- executor/executor.go | 2 +- model/model.go | 8 +-- planner/core/common_plans.go | 2 +- planner/core/planbuilder.go | 6 +- util/admin/admin.go | 130 ++++++++++------------------------- util/rowDecoder/decoder.go | 4 +- 6 files changed, 46 insertions(+), 106 deletions(-) diff --git a/executor/executor.go b/executor/executor.go index bcfacf04d31ae..1a830e07fb699 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -410,7 +410,7 @@ type CheckTableExec struct { done bool is infoschema.InfoSchema - genExprs map[string]expression.Expression + genExprs map[model.TableColumnID]expression.Expression } // Open implements the Executor Open interface. diff --git a/model/model.go b/model/model.go index dd38451877bdd..9dded4cf2327e 100644 --- a/model/model.go +++ b/model/model.go @@ -15,7 +15,6 @@ package model import ( "encoding/json" - "fmt" "strings" "time" @@ -556,7 +555,8 @@ func collationToProto(c string) int32 { return int32(mysql.DefaultCollationID) } -// GetTableColumnID gets a ID of a column with table ID -func GetTableColumnID(tableInfo *TableInfo, col *ColumnInfo) string { - return fmt.Sprintf("%d_%d", tableInfo.ID, col.ID) +// TableColumnID is composed by table ID and column ID. +type TableColumnID struct { + TableID int64 + ColumnID int64 } diff --git a/planner/core/common_plans.go b/planner/core/common_plans.go index 107e737c6aa2e..0eee933b9b773 100644 --- a/planner/core/common_plans.go +++ b/planner/core/common_plans.go @@ -73,7 +73,7 @@ type CheckTable struct { Tables []*ast.TableName - GenExprs map[string]expression.Expression + GenExprs map[model.TableColumnID]expression.Expression } // RecoverIndex is used for backfilling corrupted index data. diff --git a/planner/core/planbuilder.go b/planner/core/planbuilder.go index 20e472883f453..e4bb8000bb553 100644 --- a/planner/core/planbuilder.go +++ b/planner/core/planbuilder.go @@ -32,7 +32,6 @@ import ( "github.com/pingcap/tidb/table" "github.com/pingcap/tidb/types" "github.com/pingcap/tidb/types/parser_driver" - "github.com/pingcap/tidb/util/admin" "github.com/pingcap/tidb/util/ranger" ) @@ -530,7 +529,7 @@ func (b *planBuilder) buildAdmin(as *ast.AdminStmt) (Plan, error) { func (b *planBuilder) buildAdminCheckTable(as *ast.AdminStmt) (*CheckTable, error) { p := &CheckTable{Tables: as.Tables} - p.GenExprs = make(map[string]expression.Expression) + p.GenExprs = make(map[model.TableColumnID]expression.Expression, len(p.Tables)) mockTablePlan := LogicalTableDual{}.init(b.ctx) for _, tbl := range p.Tables { @@ -562,8 +561,7 @@ func (b *planBuilder) buildAdminCheckTable(as *ast.AdminStmt) (*CheckTable, erro return nil, errors.Trace(err) } expr = expression.BuildCastFunction(b.ctx, expr, colExpr.GetType()) - genColumnName := admin.GetTableColumnID(tableInfo, column.ColumnInfo) - p.GenExprs[genColumnName] = expr + p.GenExprs[model.TableColumnID{TableID: tableInfo.ID, ColumnID: column.ColumnInfo.ID}] = expr } } return p, nil diff --git a/util/admin/admin.go b/util/admin/admin.go index e686a367fd58b..eac0c4dced162 100644 --- a/util/admin/admin.go +++ b/util/admin/admin.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "sort" + "time" "github.com/pingcap/errors" "github.com/pingcap/parser/model" @@ -32,7 +33,7 @@ import ( "github.com/pingcap/tidb/tablecodec" "github.com/pingcap/tidb/types" "github.com/pingcap/tidb/util" - "github.com/pingcap/tidb/util/chunk" + "github.com/pingcap/tidb/util/rowDecoder" "github.com/pingcap/tidb/util/sqlexec" log "github.com/sirupsen/logrus" ) @@ -294,7 +295,7 @@ func ScanIndexData(sc *stmtctx.StatementContext, txn kv.Transaction, kvIndex tab // It returns nil if the data from the index is equal to the data from the table columns, // otherwise it returns an error with a different set of records. // genExprs is use to calculate the virtual generate column. -func CompareIndexData(sessCtx sessionctx.Context, txn kv.Transaction, t table.Table, idx table.Index, genExprs map[string]expression.Expression) error { +func CompareIndexData(sessCtx sessionctx.Context, txn kv.Transaction, t table.Table, idx table.Index, genExprs map[model.TableColumnID]expression.Expression) error { err := checkIndexAndRecord(sessCtx, txn, t, idx, genExprs) if err != nil { return errors.Trace(err) @@ -336,7 +337,7 @@ func adjustDatumKind(vals1, vals2 []types.Datum) { } } -func checkIndexAndRecord(sessCtx sessionctx.Context, txn kv.Transaction, t table.Table, idx table.Index, genExprs map[string]expression.Expression) error { +func checkIndexAndRecord(sessCtx sessionctx.Context, txn kv.Transaction, t table.Table, idx table.Index, genExprs map[model.TableColumnID]expression.Expression) error { it, err := idx.SeekFirst(txn) if err != nil { return errors.Trace(err) @@ -352,6 +353,7 @@ func checkIndexAndRecord(sessCtx sessionctx.Context, txn kv.Transaction, t table if err != nil { return errors.Trace(err) } + rowDecoder := makeRowDecoder(t, cols, genExprs) sc := sessCtx.GetSessionVars().StmtCtx for { vals1, h, err := it.Next() @@ -365,7 +367,7 @@ func checkIndexAndRecord(sessCtx sessionctx.Context, txn kv.Transaction, t table if err != nil { return errors.Trace(err) } - vals2, err := rowWithCols(sessCtx, txn, t, h, cols, genExprs) + vals2, err := rowWithCols(sessCtx, txn, t, h, cols, rowDecoder) vals2 = tables.TruncateIndexValuesIfNeeded(t.Meta(), idx.Meta(), vals2) if kv.ErrNotExist.Equal(err) { record := &RecordData{Handle: h, Values: vals1} @@ -399,7 +401,7 @@ func compareDatumSlice(sc *stmtctx.StatementContext, val1s, val2s []types.Datum) } // CheckRecordAndIndex is exported for testing. -func CheckRecordAndIndex(sessCtx sessionctx.Context, txn kv.Transaction, t table.Table, idx table.Index, genExprs map[string]expression.Expression) error { +func CheckRecordAndIndex(sessCtx sessionctx.Context, txn kv.Transaction, t table.Table, idx table.Index, genExprs map[model.TableColumnID]expression.Expression) error { sc := sessCtx.GetSessionVars().StmtCtx cols := make([]*table.Column, len(idx.Meta().Columns)) for i, col := range idx.Meta().Columns { @@ -551,16 +553,38 @@ func CompareTableRecord(sessCtx sessionctx.Context, txn kv.Transaction, t table. return nil } +func makeRowDecoder(t table.Table, decodeCol []*table.Column, genExpr map[model.TableColumnID]expression.Expression) decoder.RowDecoder { + cols := t.Cols() + tblInfo := t.Meta() + decodeColsMap := make(map[int64]decoder.Column, len(decodeCol)) + for _, v := range decodeCol { + col := cols[v.Offset] + tpExpr := decoder.Column{ + Info: col.ToInfo(), + } + if col.IsGenerated() && !col.GeneratedStored { + for _, c := range cols { + if _, ok := col.Dependences[c.Name.L]; ok { + decodeColsMap[c.ID] = decoder.Column{ + Info: c.ToInfo(), + } + } + } + tpExpr.GenExpr = genExpr[model.TableColumnID{TableID: tblInfo.ID, ColumnID: col.ID}] + } + decodeColsMap[col.ID] = tpExpr + } + return decoder.NewRowDecoder(cols, decodeColsMap) +} + // genExprs use to calculate generated column value. -func rowWithCols(sessCtx sessionctx.Context, txn kv.Retriever, t table.Table, h int64, cols []*table.Column, genExprs map[string]expression.Expression) ([]types.Datum, error) { +func rowWithCols(sessCtx sessionctx.Context, txn kv.Retriever, t table.Table, h int64, cols []*table.Column, rowDecoder decoder.RowDecoder) ([]types.Datum, error) { key := t.RecordKey(h) value, err := txn.Get(key) - genColFlag := false if err != nil { return nil, errors.Trace(err) } v := make([]types.Datum, len(cols)) - colTps := make(map[int64]*types.FieldType, len(cols)) for i, col := range cols { if col == nil { continue @@ -576,34 +600,13 @@ func rowWithCols(sessCtx sessionctx.Context, txn kv.Retriever, t table.Table, h } continue } - // If have virtual generate column , decode all columns. - if col.IsGenerated() && col.GeneratedStored == false { - genColFlag = true - } - colTps[col.ID] = &col.FieldType - } - // if have virtual generate column, decode all columns - if genColFlag { - for _, c := range t.Cols() { - if c.State != model.StatePublic { - continue - } - colTps[c.ID] = &c.FieldType - } } - rowMap, err := tablecodec.DecodeRow(value, colTps, sessCtx.GetSessionVars().Location()) + rowMap, err := rowDecoder.DecodeAndEvalRowWithMap(sessCtx, value, sessCtx.GetSessionVars().Location(), time.UTC, nil) if err != nil { return nil, errors.Trace(err) } - if genColFlag && genExprs != nil { - err = fillGenColData(sessCtx, rowMap, t, cols, genExprs) - if err != nil { - return v, errors.Trace(err) - } - } - for i, col := range cols { if col == nil { continue @@ -635,7 +638,7 @@ func rowWithCols(sessCtx sessionctx.Context, txn kv.Retriever, t table.Table, h // genExprs use to calculate generated column value. func iterRecords(sessCtx sessionctx.Context, retriever kv.Retriever, t table.Table, startKey kv.Key, cols []*table.Column, - fn table.RecordIterFunc, genExprs map[string]expression.Expression) error { + fn table.RecordIterFunc, genExprs map[model.TableColumnID]expression.Expression) error { prefix := t.RecordPrefix() keyUpperBound := prefix.PrefixNext() @@ -650,22 +653,7 @@ func iterRecords(sessCtx sessionctx.Context, retriever kv.Retriever, t table.Tab } log.Debugf("startKey:%q, key:%q, value:%q", startKey, it.Key(), it.Value()) - - genColFlag := false - colMap := make(map[int64]*types.FieldType, len(cols)) - for _, col := range cols { - if col.IsGenerated() && col.GeneratedStored == false { - genColFlag = true - break - } - colMap[col.ID] = &col.FieldType - } - if genColFlag { - for _, col := range t.Cols() { - colMap[col.ID] = &col.FieldType - } - } - + rowDecoder := makeRowDecoder(t, cols, genExprs) for it.Valid() && it.Key().HasPrefix(prefix) { // first kv pair is row lock information. // TODO: check valid lock @@ -675,18 +663,10 @@ func iterRecords(sessCtx sessionctx.Context, retriever kv.Retriever, t table.Tab return errors.Trace(err) } - rowMap, err := tablecodec.DecodeRow(it.Value(), colMap, sessCtx.GetSessionVars().Location()) + rowMap, err := rowDecoder.DecodeAndEvalRowWithMap(sessCtx, it.Value(), sessCtx.GetSessionVars().Location(), time.UTC, nil) if err != nil { return errors.Trace(err) } - - if genColFlag && genExprs != nil { - err = fillGenColData(sessCtx, rowMap, t, cols, genExprs) - if err != nil { - return errors.Trace(err) - } - } - data := make([]types.Datum, 0, len(cols)) for _, col := range cols { if col.IsPKHandleColumn(t.Meta()) { @@ -714,44 +694,6 @@ func iterRecords(sessCtx sessionctx.Context, retriever kv.Retriever, t table.Tab return nil } -// genExprs use to calculate generated column value. -func fillGenColData(sessCtx sessionctx.Context, rowMap map[int64]types.Datum, t table.Table, cols []*table.Column, genExprs map[string]expression.Expression) error { - tableInfo := t.Meta() - row := make([]types.Datum, len(t.Cols())) - for _, col := range t.Cols() { - ri, ok := rowMap[col.ID] - if ok { - row[col.Offset] = ri - } - } - - var err error - for _, col := range cols { - if !col.IsGenerated() || col.GeneratedStored == true { - continue - } - genColumnName := GetTableColumnID(tableInfo, col.ColumnInfo) - if expr, ok := genExprs[genColumnName]; ok { - var val types.Datum - val, err = expr.Eval(chunk.MutRowFromDatums(row).ToRow()) - if err != nil { - return errors.Trace(err) - } - val, err = table.CastValue(sessCtx, val, col.ToInfo()) - if err != nil { - return errors.Trace(err) - } - rowMap[col.ID] = val - } - } - return nil -} - -// GetTableColumnID gets a ID of a column with table ID // TableColumnID is composed by table ID and column ID. -func GetTableColumnID(tableInfo *model.TableInfo, col *model.ColumnInfo) string { - return fmt.Sprintf("%d_%d", tableInfo.ID, col.ID) -} - // admin error codes. const ( codeDataNotEqual terror.ErrCode = 1 diff --git a/util/rowDecoder/decoder.go b/util/rowDecoder/decoder.go index 3b99f7a16126a..9cfb1947e8f1d 100644 --- a/util/rowDecoder/decoder.go +++ b/util/rowDecoder/decoder.go @@ -96,9 +96,9 @@ func (rd RowDecoder) DecodeAndEvalRowWithMap(ctx sessionctx.Context, b []byte, d return nil, errors.Trace(err) } - if val.Kind() == types.KindMysqlTime { + if val.Kind() == types.KindMysqlTime && sysLoc != time.UTC { t := val.GetMysqlTime() - if t.Type == mysql.TypeTimestamp && sysLoc != time.UTC { + if t.Type == mysql.TypeTimestamp { err := t.ConvertTimeZone(sysLoc, time.UTC) if err != nil { return nil, errors.Trace(err) From cf2d7e69a23ad45d7bfa7160783b5693496670d0 Mon Sep 17 00:00:00 2001 From: winkyao Date: Tue, 11 Dec 2018 18:08:18 +0800 Subject: [PATCH 2/5] ddl: fix panic when add index of generated column. (#8620) --- ddl/db_integration_test.go | 18 ++++++++++++++++++ ddl/index.go | 16 +++++++++++++--- tablecodec/tablecodec.go | 4 ++-- tablecodec/tablecodec_test.go | 2 +- util/admin/admin.go | 4 ++-- util/rowDecoder/decoder.go | 8 ++++---- 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/ddl/db_integration_test.go b/ddl/db_integration_test.go index 0691d4242aafb..185b3c774067e 100644 --- a/ddl/db_integration_test.go +++ b/ddl/db_integration_test.go @@ -152,6 +152,24 @@ func (s *testIntegrationSuite) TestEndIncluded(c *C) { tk.MustExec("admin check table t") } +func (s *testIntegrationSuite) TestNullGeneratedColumn(c *C) { + tk := testkit.NewTestKit(c, s.store) + + tk.MustExec("use test") + tk.MustExec("drop table if exists t") + tk.MustExec("CREATE TABLE `t` (" + + "`a` int(11) DEFAULT NULL," + + "`b` int(11) DEFAULT NULL," + + "`c` int(11) GENERATED ALWAYS AS (`a` + `b`) VIRTUAL DEFAULT NULL," + + "`h` varchar(10) DEFAULT NULL," + + "`m` int(11) DEFAULT NULL" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin") + + tk.MustExec("insert into t values()") + tk.MustExec("alter table t add index idx_c(c)") + tk.MustExec("drop table t") +} + func (s *testIntegrationSuite) TestCaseInsensitiveCharsetAndCollate(c *C) { tk := testkit.NewTestKit(c, s.store) diff --git a/ddl/index.go b/ddl/index.go index a97a1c32cb337..b04c0199fedd4 100644 --- a/ddl/index.go +++ b/ddl/index.go @@ -456,7 +456,7 @@ type addIndexWorker struct { defaultVals []types.Datum idxRecords []*indexRecord rowMap map[int64]types.Datum - rowDecoder decoder.RowDecoder + rowDecoder *decoder.RowDecoder idxKeyBufs [][]byte batchCheckKeys []kv.Key distinctCheckFlags []bool @@ -542,8 +542,8 @@ func (w *addIndexWorker) getIndexRecord(handle int64, recordKey []byte, rawRecor } continue } - idxColumnVal := w.rowMap[col.ID] - if _, ok := w.rowMap[col.ID]; ok { + idxColumnVal, ok := w.rowMap[col.ID] + if ok { idxVal[j] = idxColumnVal // Make sure there is no dirty data. delete(w.rowMap, col.ID) @@ -566,10 +566,19 @@ func (w *addIndexWorker) getIndexRecord(handle int64, recordKey []byte, rawRecor } idxVal[j] = idxColumnVal } + // If there are generated column, rowDecoder will use column value that not in idxInfo.Columns to calculate + // the generated value, so we need to clear up the reusing map. + w.cleanRowMap() idxRecord := &indexRecord{handle: handle, key: recordKey, vals: idxVal} return idxRecord, nil } +func (w *addIndexWorker) cleanRowMap() { + for id := range w.rowMap { + delete(w.rowMap, id) + } +} + // getNextHandle gets next handle of entry that we are going to process. func (w *addIndexWorker) getNextHandle(taskRange reorgIndexTask, taskDone bool) (nextHandle int64) { if !taskDone { @@ -788,6 +797,7 @@ func (w *addIndexWorker) handleBackfillTask(d *ddlCtx, task *reorgIndexTask) *ad // we should check whether this ddl job is still runnable. err = w.ddlWorker.isReorgRunnable(d) } + if err != nil { result.err = err return result diff --git a/tablecodec/tablecodec.go b/tablecodec/tablecodec.go index de9b31c31210e..7eeea8e57cb59 100644 --- a/tablecodec/tablecodec.go +++ b/tablecodec/tablecodec.go @@ -305,10 +305,10 @@ func DecodeRowWithMap(b []byte, cols map[int64]*types.FieldType, loc *time.Locat row = make(map[int64]types.Datum, len(cols)) } if b == nil { - return nil, nil + return row, nil } if len(b) == 1 && b[0] == codec.NilFlag { - return nil, nil + return row, nil } cnt := 0 var ( diff --git a/tablecodec/tablecodec_test.go b/tablecodec/tablecodec_test.go index a68f26d6ef873..9467d926128d9 100644 --- a/tablecodec/tablecodec_test.go +++ b/tablecodec/tablecodec_test.go @@ -137,7 +137,7 @@ func (s *testTableCodecSuite) TestRowCodec(c *C) { r, err = DecodeRow(bs, colMap, time.UTC) c.Assert(err, IsNil) - c.Assert(r, IsNil) + c.Assert(len(r), Equals, 0) } func (s *testTableCodecSuite) TestTimeCodec(c *C) { diff --git a/util/admin/admin.go b/util/admin/admin.go index eac0c4dced162..ae35073e1f8dd 100644 --- a/util/admin/admin.go +++ b/util/admin/admin.go @@ -553,7 +553,7 @@ func CompareTableRecord(sessCtx sessionctx.Context, txn kv.Transaction, t table. return nil } -func makeRowDecoder(t table.Table, decodeCol []*table.Column, genExpr map[model.TableColumnID]expression.Expression) decoder.RowDecoder { +func makeRowDecoder(t table.Table, decodeCol []*table.Column, genExpr map[model.TableColumnID]expression.Expression) *decoder.RowDecoder { cols := t.Cols() tblInfo := t.Meta() decodeColsMap := make(map[int64]decoder.Column, len(decodeCol)) @@ -578,7 +578,7 @@ func makeRowDecoder(t table.Table, decodeCol []*table.Column, genExpr map[model. } // genExprs use to calculate generated column value. -func rowWithCols(sessCtx sessionctx.Context, txn kv.Retriever, t table.Table, h int64, cols []*table.Column, rowDecoder decoder.RowDecoder) ([]types.Datum, error) { +func rowWithCols(sessCtx sessionctx.Context, txn kv.Retriever, t table.Table, h int64, cols []*table.Column, rowDecoder *decoder.RowDecoder) ([]types.Datum, error) { key := t.RecordKey(h) value, err := txn.Get(key) if err != nil { diff --git a/util/rowDecoder/decoder.go b/util/rowDecoder/decoder.go index 9cfb1947e8f1d..5e83e535d54ae 100644 --- a/util/rowDecoder/decoder.go +++ b/util/rowDecoder/decoder.go @@ -42,7 +42,7 @@ type RowDecoder struct { } // NewRowDecoder returns a new RowDecoder. -func NewRowDecoder(cols []*table.Column, decodeColMap map[int64]Column) RowDecoder { +func NewRowDecoder(cols []*table.Column, decodeColMap map[int64]Column) *RowDecoder { colFieldMap := make(map[int64]*types.FieldType, len(decodeColMap)) haveGenCol := false for id, col := range decodeColMap { @@ -52,7 +52,7 @@ func NewRowDecoder(cols []*table.Column, decodeColMap map[int64]Column) RowDecod } } if !haveGenCol { - return RowDecoder{ + return &RowDecoder{ colTypes: colFieldMap, } } @@ -61,7 +61,7 @@ func NewRowDecoder(cols []*table.Column, decodeColMap map[int64]Column) RowDecod for _, col := range cols { tps[col.Offset] = &col.FieldType } - return RowDecoder{ + return &RowDecoder{ mutRow: chunk.MutRowFromTypes(tps), columns: decodeColMap, colTypes: colFieldMap, @@ -70,7 +70,7 @@ func NewRowDecoder(cols []*table.Column, decodeColMap map[int64]Column) RowDecod } // DecodeAndEvalRowWithMap decodes a byte slice into datums and evaluates the generated column value. -func (rd RowDecoder) DecodeAndEvalRowWithMap(ctx sessionctx.Context, b []byte, decodeLoc, sysLoc *time.Location, row map[int64]types.Datum) (map[int64]types.Datum, error) { +func (rd *RowDecoder) DecodeAndEvalRowWithMap(ctx sessionctx.Context, b []byte, decodeLoc, sysLoc *time.Location, row map[int64]types.Datum) (map[int64]types.Datum, error) { row, err := tablecodec.DecodeRowWithMap(b, rd.colTypes, decodeLoc, row) if err != nil { return nil, errors.Trace(err) From a494587312a05573da2e64e35eede5e636feafc4 Mon Sep 17 00:00:00 2001 From: crazycs520 Date: Tue, 18 Dec 2018 12:41:02 +0800 Subject: [PATCH 3/5] fix circle ci --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9c219f4b8e54..7923eda025b14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: go go_import_path: github.com/pingcap/tidb go: - - "1.11" + - "1.11.2" env: - TRAVIS_COVERAGE=0 From 87adf74c04fa56fe2625e9ca8cedb340ea1604e1 Mon Sep 17 00:00:00 2001 From: crazycs520 Date: Tue, 18 Dec 2018 12:45:28 +0800 Subject: [PATCH 4/5] fix circle ci, use go 1.11.3 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7923eda025b14..44f9bb39f341a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: go go_import_path: github.com/pingcap/tidb go: - - "1.11.2" + - "1.11.3" env: - TRAVIS_COVERAGE=0 From 8f2db68f913bcd8327d3ef77698200049f460535 Mon Sep 17 00:00:00 2001 From: crazycs520 Date: Tue, 18 Dec 2018 13:00:07 +0800 Subject: [PATCH 5/5] fix circle ci, use go 1.11.3 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 756b93c9fa0c6..b7ba79c82b84b 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ version: 2 jobs: build: docker: - - image: golang:1.11 + - image: golang:1.11.3 working_directory: /go/src/github.com/pingcap/tidb steps: - checkout