github.com/pingcap/ticdc@v0.0.0-20220526033649-485a10ef2652/cdc/entry/schema_storage_test.go (about)

     1  // Copyright 2020 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package entry
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"sort"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/pingcap/check"
    23  	"github.com/pingcap/errors"
    24  	timodel "github.com/pingcap/parser/model"
    25  	"github.com/pingcap/parser/mysql"
    26  	"github.com/pingcap/ticdc/cdc/kv"
    27  	"github.com/pingcap/ticdc/cdc/model"
    28  	"github.com/pingcap/ticdc/pkg/util/testleak"
    29  	ticonfig "github.com/pingcap/tidb/config"
    30  	"github.com/pingcap/tidb/domain"
    31  	tidbkv "github.com/pingcap/tidb/kv"
    32  	timeta "github.com/pingcap/tidb/meta"
    33  	"github.com/pingcap/tidb/session"
    34  	"github.com/pingcap/tidb/sessionctx"
    35  	"github.com/pingcap/tidb/store/mockstore"
    36  	"github.com/pingcap/tidb/store/tikv/oracle"
    37  	"github.com/pingcap/tidb/types"
    38  	"github.com/pingcap/tidb/util/testkit"
    39  )
    40  
    41  type schemaSuite struct{}
    42  
    43  var _ = check.Suite(&schemaSuite{})
    44  
    45  func (t *schemaSuite) TestSchema(c *check.C) {
    46  	defer testleak.AfterTest(c)()
    47  	dbName := timodel.NewCIStr("Test")
    48  	// db and ignoreDB info
    49  	dbInfo := &timodel.DBInfo{
    50  		ID:    1,
    51  		Name:  dbName,
    52  		State: timodel.StatePublic,
    53  	}
    54  	// `createSchema` job1
    55  	job := &timodel.Job{
    56  		ID:         3,
    57  		State:      timodel.JobStateSynced,
    58  		SchemaID:   1,
    59  		Type:       timodel.ActionCreateSchema,
    60  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 1, DBInfo: dbInfo, FinishedTS: 123},
    61  		Query:      "create database test",
    62  	}
    63  	// reconstruct the local schema
    64  	snap := newEmptySchemaSnapshot(false)
    65  	err := snap.handleDDL(job)
    66  	c.Assert(err, check.IsNil)
    67  	_, exist := snap.SchemaByID(job.SchemaID)
    68  	c.Assert(exist, check.IsTrue)
    69  
    70  	// test drop schema
    71  	job = &timodel.Job{
    72  		ID:         6,
    73  		State:      timodel.JobStateSynced,
    74  		SchemaID:   1,
    75  		Type:       timodel.ActionDropSchema,
    76  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 3, DBInfo: dbInfo, FinishedTS: 124},
    77  		Query:      "drop database test",
    78  	}
    79  	err = snap.handleDDL(job)
    80  	c.Assert(err, check.IsNil)
    81  	_, exist = snap.SchemaByID(job.SchemaID)
    82  	c.Assert(exist, check.IsFalse)
    83  
    84  	job = &timodel.Job{
    85  		ID:         3,
    86  		State:      timodel.JobStateSynced,
    87  		SchemaID:   1,
    88  		Type:       timodel.ActionCreateSchema,
    89  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 2, DBInfo: dbInfo, FinishedTS: 124},
    90  		Query:      "create database test",
    91  	}
    92  
    93  	err = snap.handleDDL(job)
    94  	c.Assert(err, check.IsNil)
    95  	err = snap.handleDDL(job)
    96  	c.Log(err)
    97  	c.Assert(errors.IsAlreadyExists(err), check.IsTrue)
    98  
    99  	// test schema drop schema error
   100  	job = &timodel.Job{
   101  		ID:         9,
   102  		State:      timodel.JobStateSynced,
   103  		SchemaID:   1,
   104  		Type:       timodel.ActionDropSchema,
   105  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 1, DBInfo: dbInfo, FinishedTS: 123},
   106  		Query:      "drop database test",
   107  	}
   108  	err = snap.handleDDL(job)
   109  	c.Assert(err, check.IsNil)
   110  	err = snap.handleDDL(job)
   111  	c.Assert(errors.IsNotFound(err), check.IsTrue)
   112  }
   113  
   114  func (*schemaSuite) TestTable(c *check.C) {
   115  	defer testleak.AfterTest(c)()
   116  	var jobs []*timodel.Job
   117  	dbName := timodel.NewCIStr("Test")
   118  	tbName := timodel.NewCIStr("T")
   119  	colName := timodel.NewCIStr("A")
   120  	idxName := timodel.NewCIStr("idx")
   121  	// column info
   122  	colInfo := &timodel.ColumnInfo{
   123  		ID:        1,
   124  		Name:      colName,
   125  		Offset:    0,
   126  		FieldType: *types.NewFieldType(mysql.TypeLonglong),
   127  		State:     timodel.StatePublic,
   128  	}
   129  	// index info
   130  	idxInfo := &timodel.IndexInfo{
   131  		Name:  idxName,
   132  		Table: tbName,
   133  		Columns: []*timodel.IndexColumn{
   134  			{
   135  				Name:   colName,
   136  				Offset: 0,
   137  				Length: 10,
   138  			},
   139  		},
   140  		Unique:  true,
   141  		Primary: true,
   142  		State:   timodel.StatePublic,
   143  	}
   144  	// table info
   145  	tblInfo := &timodel.TableInfo{
   146  		ID:    2,
   147  		Name:  tbName,
   148  		State: timodel.StatePublic,
   149  	}
   150  	// db info
   151  	dbInfo := &timodel.DBInfo{
   152  		ID:    3,
   153  		Name:  dbName,
   154  		State: timodel.StatePublic,
   155  	}
   156  
   157  	// `createSchema` job
   158  	job := &timodel.Job{
   159  		ID:         5,
   160  		State:      timodel.JobStateSynced,
   161  		SchemaID:   3,
   162  		Type:       timodel.ActionCreateSchema,
   163  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 1, DBInfo: dbInfo, FinishedTS: 123},
   164  		Query:      "create database " + dbName.O,
   165  	}
   166  	jobs = append(jobs, job)
   167  
   168  	// `createTable` job
   169  	job = &timodel.Job{
   170  		ID:         6,
   171  		State:      timodel.JobStateSynced,
   172  		SchemaID:   3,
   173  		TableID:    2,
   174  		Type:       timodel.ActionCreateTable,
   175  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 2, TableInfo: tblInfo, FinishedTS: 124},
   176  		Query:      "create table " + tbName.O,
   177  	}
   178  	jobs = append(jobs, job)
   179  
   180  	// `addColumn` job
   181  	tblInfo.Columns = []*timodel.ColumnInfo{colInfo}
   182  	job = &timodel.Job{
   183  		ID:         7,
   184  		State:      timodel.JobStateSynced,
   185  		SchemaID:   3,
   186  		TableID:    2,
   187  		Type:       timodel.ActionAddColumn,
   188  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 3, TableInfo: tblInfo, FinishedTS: 125},
   189  		Query:      "alter table " + tbName.O + " add column " + colName.O,
   190  	}
   191  	jobs = append(jobs, job)
   192  
   193  	// construct a historical `addIndex` job
   194  	tblInfo = tblInfo.Clone()
   195  	tblInfo.Indices = []*timodel.IndexInfo{idxInfo}
   196  	job = &timodel.Job{
   197  		ID:         8,
   198  		State:      timodel.JobStateSynced,
   199  		SchemaID:   3,
   200  		TableID:    2,
   201  		Type:       timodel.ActionAddIndex,
   202  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 4, TableInfo: tblInfo, FinishedTS: 126},
   203  		Query:      fmt.Sprintf("alter table %s add index %s(%s)", tbName, idxName, colName),
   204  	}
   205  	jobs = append(jobs, job)
   206  
   207  	// reconstruct the local schema
   208  	snap := newEmptySchemaSnapshot(false)
   209  	for _, job := range jobs {
   210  		err := snap.handleDDL(job)
   211  		c.Assert(err, check.IsNil)
   212  	}
   213  
   214  	// check the historical db that constructed above whether in the schema list of local schema
   215  	_, ok := snap.SchemaByID(dbInfo.ID)
   216  	c.Assert(ok, check.IsTrue)
   217  	// check the historical table that constructed above whether in the table list of local schema
   218  	table, ok := snap.TableByID(tblInfo.ID)
   219  	c.Assert(ok, check.IsTrue)
   220  	c.Assert(table.Columns, check.HasLen, 1)
   221  	c.Assert(table.Indices, check.HasLen, 1)
   222  
   223  	// test ineligible tables
   224  	c.Assert(snap.IsIneligibleTableID(2), check.IsTrue)
   225  
   226  	// check truncate table
   227  	tblInfo1 := &timodel.TableInfo{
   228  		ID:    9,
   229  		Name:  tbName,
   230  		State: timodel.StatePublic,
   231  	}
   232  	job = &timodel.Job{
   233  		ID:         9,
   234  		State:      timodel.JobStateSynced,
   235  		SchemaID:   3,
   236  		TableID:    2,
   237  		Type:       timodel.ActionTruncateTable,
   238  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 5, TableInfo: tblInfo1, FinishedTS: 127},
   239  		Query:      "truncate table " + tbName.O,
   240  	}
   241  	preTableInfo, err := snap.PreTableInfo(job)
   242  	c.Assert(err, check.IsNil)
   243  	c.Assert(preTableInfo.TableName, check.Equals, model.TableName{Schema: "Test", Table: "T"})
   244  	c.Assert(preTableInfo.ID, check.Equals, int64(2))
   245  
   246  	err = snap.handleDDL(job)
   247  	c.Assert(err, check.IsNil)
   248  
   249  	_, ok = snap.TableByID(tblInfo1.ID)
   250  	c.Assert(ok, check.IsTrue)
   251  
   252  	_, ok = snap.TableByID(2)
   253  	c.Assert(ok, check.IsFalse)
   254  
   255  	// test ineligible tables
   256  	c.Assert(snap.IsIneligibleTableID(9), check.IsTrue)
   257  	c.Assert(snap.IsIneligibleTableID(2), check.IsFalse)
   258  	// check drop table
   259  	job = &timodel.Job{
   260  		ID:         9,
   261  		State:      timodel.JobStateSynced,
   262  		SchemaID:   3,
   263  		TableID:    9,
   264  		Type:       timodel.ActionDropTable,
   265  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 6, FinishedTS: 128},
   266  		Query:      "drop table " + tbName.O,
   267  	}
   268  	preTableInfo, err = snap.PreTableInfo(job)
   269  	c.Assert(err, check.IsNil)
   270  	c.Assert(preTableInfo.TableName, check.Equals, model.TableName{Schema: "Test", Table: "T"})
   271  	c.Assert(preTableInfo.ID, check.Equals, int64(9))
   272  
   273  	err = snap.handleDDL(job)
   274  	c.Assert(err, check.IsNil)
   275  
   276  	_, ok = snap.TableByID(tblInfo.ID)
   277  	c.Assert(ok, check.IsFalse)
   278  
   279  	// test ineligible tables
   280  	c.Assert(snap.IsIneligibleTableID(9), check.IsFalse)
   281  
   282  	// drop schema
   283  	err = snap.dropSchema(3)
   284  	c.Assert(err, check.IsNil)
   285  }
   286  
   287  func (t *schemaSuite) TestHandleDDL(c *check.C) {
   288  	defer testleak.AfterTest(c)()
   289  
   290  	snap := newEmptySchemaSnapshot(false)
   291  	dbName := timodel.NewCIStr("Test")
   292  	colName := timodel.NewCIStr("A")
   293  	tbName := timodel.NewCIStr("T")
   294  	newTbName := timodel.NewCIStr("RT")
   295  
   296  	// db info
   297  	dbInfo := &timodel.DBInfo{
   298  		ID:    2,
   299  		Name:  dbName,
   300  		State: timodel.StatePublic,
   301  	}
   302  	// table Info
   303  	tblInfo := &timodel.TableInfo{
   304  		ID:    6,
   305  		Name:  tbName,
   306  		State: timodel.StatePublic,
   307  	}
   308  	// column info
   309  	colInfo := &timodel.ColumnInfo{
   310  		ID:        8,
   311  		Name:      colName,
   312  		Offset:    0,
   313  		FieldType: *types.NewFieldType(mysql.TypeLonglong),
   314  		State:     timodel.StatePublic,
   315  	}
   316  	tblInfo.Columns = []*timodel.ColumnInfo{colInfo}
   317  
   318  	testCases := []struct {
   319  		name        string
   320  		jobID       int64
   321  		schemaID    int64
   322  		tableID     int64
   323  		jobType     timodel.ActionType
   324  		binlogInfo  *timodel.HistoryInfo
   325  		query       string
   326  		resultQuery string
   327  		schemaName  string
   328  		tableName   string
   329  	}{
   330  		{name: "createSchema", jobID: 3, schemaID: 2, tableID: 0, jobType: timodel.ActionCreateSchema, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 1, DBInfo: dbInfo, TableInfo: nil, FinishedTS: 123}, query: "create database Test", resultQuery: "create database Test", schemaName: dbInfo.Name.O, tableName: ""},
   331  		{name: "updateSchema", jobID: 4, schemaID: 2, tableID: 0, jobType: timodel.ActionModifySchemaCharsetAndCollate, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 8, DBInfo: dbInfo, TableInfo: nil, FinishedTS: 123}, query: "ALTER DATABASE Test CHARACTER SET utf8mb4;", resultQuery: "ALTER DATABASE Test CHARACTER SET utf8mb4;", schemaName: dbInfo.Name.O},
   332  		{name: "createTable", jobID: 7, schemaID: 2, tableID: 6, jobType: timodel.ActionCreateTable, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 3, DBInfo: nil, TableInfo: tblInfo, FinishedTS: 123}, query: "create table T(id int);", resultQuery: "create table T(id int);", schemaName: dbInfo.Name.O, tableName: tblInfo.Name.O},
   333  		{name: "addColumn", jobID: 9, schemaID: 2, tableID: 6, jobType: timodel.ActionAddColumn, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 4, DBInfo: nil, TableInfo: tblInfo, FinishedTS: 123}, query: "alter table T add a varchar(45);", resultQuery: "alter table T add a varchar(45);", schemaName: dbInfo.Name.O, tableName: tblInfo.Name.O},
   334  		{name: "truncateTable", jobID: 10, schemaID: 2, tableID: 6, jobType: timodel.ActionTruncateTable, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 5, DBInfo: nil, TableInfo: tblInfo, FinishedTS: 123}, query: "truncate table T;", resultQuery: "truncate table T;", schemaName: dbInfo.Name.O, tableName: tblInfo.Name.O},
   335  		{name: "renameTable", jobID: 11, schemaID: 2, tableID: 10, jobType: timodel.ActionRenameTable, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 6, DBInfo: nil, TableInfo: tblInfo, FinishedTS: 123}, query: "rename table T to RT;", resultQuery: "rename table T to RT;", schemaName: dbInfo.Name.O, tableName: newTbName.O},
   336  		{name: "dropTable", jobID: 12, schemaID: 2, tableID: 12, jobType: timodel.ActionDropTable, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 7, DBInfo: nil, TableInfo: nil, FinishedTS: 123}, query: "drop table RT;", resultQuery: "drop table RT;", schemaName: dbInfo.Name.O, tableName: newTbName.O},
   337  		{name: "dropSchema", jobID: 13, schemaID: 2, tableID: 0, jobType: timodel.ActionDropSchema, binlogInfo: &timodel.HistoryInfo{SchemaVersion: 8, DBInfo: dbInfo, TableInfo: nil, FinishedTS: 123}, query: "drop database test;", resultQuery: "drop database test;", schemaName: dbInfo.Name.O, tableName: ""},
   338  	}
   339  
   340  	for _, testCase := range testCases {
   341  		// prepare for ddl
   342  		switch testCase.name {
   343  		case "addColumn":
   344  			tblInfo.Columns = []*timodel.ColumnInfo{colInfo}
   345  		case "truncateTable":
   346  			tblInfo.ID = 10
   347  		case "renameTable":
   348  			tblInfo.ID = 12
   349  			tblInfo.Name = newTbName
   350  		}
   351  
   352  		job := &timodel.Job{
   353  			ID:         testCase.jobID,
   354  			State:      timodel.JobStateDone,
   355  			SchemaID:   testCase.schemaID,
   356  			TableID:    testCase.tableID,
   357  			Type:       testCase.jobType,
   358  			BinlogInfo: testCase.binlogInfo,
   359  			Query:      testCase.query,
   360  		}
   361  		testDoDDLAndCheck(c, snap, job, false)
   362  
   363  		// custom check after ddl
   364  		switch testCase.name {
   365  		case "createSchema":
   366  			_, ok := snap.SchemaByID(dbInfo.ID)
   367  			c.Assert(ok, check.IsTrue)
   368  		case "createTable":
   369  			_, ok := snap.TableByID(tblInfo.ID)
   370  			c.Assert(ok, check.IsTrue)
   371  		case "renameTable":
   372  			tb, ok := snap.TableByID(tblInfo.ID)
   373  			c.Assert(ok, check.IsTrue)
   374  			c.Assert(tblInfo.Name, check.Equals, tb.Name)
   375  		case "addColumn", "truncateTable":
   376  			tb, ok := snap.TableByID(tblInfo.ID)
   377  			c.Assert(ok, check.IsTrue)
   378  			c.Assert(tb.Columns, check.HasLen, 1)
   379  		case "dropTable":
   380  			_, ok := snap.TableByID(tblInfo.ID)
   381  			c.Assert(ok, check.IsFalse)
   382  		case "dropSchema":
   383  			_, ok := snap.SchemaByID(job.SchemaID)
   384  			c.Assert(ok, check.IsFalse)
   385  		}
   386  	}
   387  }
   388  
   389  func testDoDDLAndCheck(c *check.C, snap *schemaSnapshot, job *timodel.Job, isErr bool) {
   390  	err := snap.handleDDL(job)
   391  	c.Assert(err != nil, check.Equals, isErr)
   392  }
   393  
   394  type getUniqueKeysSuite struct{}
   395  
   396  var _ = check.Suite(&getUniqueKeysSuite{})
   397  
   398  func (s *getUniqueKeysSuite) TestPKShouldBeInTheFirstPlaceWhenPKIsNotHandle(c *check.C) {
   399  	defer testleak.AfterTest(c)()
   400  	t := timodel.TableInfo{
   401  		Columns: []*timodel.ColumnInfo{
   402  			{
   403  				Name: timodel.CIStr{O: "name"},
   404  				FieldType: types.FieldType{
   405  					Flag: mysql.NotNullFlag,
   406  				},
   407  			},
   408  			{Name: timodel.CIStr{O: "id"}},
   409  		},
   410  		Indices: []*timodel.IndexInfo{
   411  			{
   412  				Name: timodel.CIStr{
   413  					O: "name",
   414  				},
   415  				Columns: []*timodel.IndexColumn{
   416  					{
   417  						Name:   timodel.CIStr{O: "name"},
   418  						Offset: 0,
   419  					},
   420  				},
   421  				Unique: true,
   422  			},
   423  			{
   424  				Name: timodel.CIStr{
   425  					O: "PRIMARY",
   426  				},
   427  				Columns: []*timodel.IndexColumn{
   428  					{
   429  						Name:   timodel.CIStr{O: "id"},
   430  						Offset: 1,
   431  					},
   432  				},
   433  				Primary: true,
   434  			},
   435  		},
   436  		PKIsHandle: false,
   437  	}
   438  	info := model.WrapTableInfo(1, "", 0, &t)
   439  	cols := info.GetUniqueKeys()
   440  	c.Assert(cols, check.DeepEquals, [][]string{
   441  		{"id"}, {"name"},
   442  	})
   443  }
   444  
   445  func (s *getUniqueKeysSuite) TestPKShouldBeInTheFirstPlaceWhenPKIsHandle(c *check.C) {
   446  	defer testleak.AfterTest(c)()
   447  	t := timodel.TableInfo{
   448  		Indices: []*timodel.IndexInfo{
   449  			{
   450  				Name: timodel.CIStr{
   451  					O: "uniq_job",
   452  				},
   453  				Columns: []*timodel.IndexColumn{
   454  					{Name: timodel.CIStr{O: "job"}},
   455  				},
   456  				Unique: true,
   457  			},
   458  		},
   459  		Columns: []*timodel.ColumnInfo{
   460  			{
   461  				Name: timodel.CIStr{
   462  					O: "job",
   463  				},
   464  				FieldType: types.FieldType{
   465  					Flag: mysql.NotNullFlag,
   466  				},
   467  			},
   468  			{
   469  				Name: timodel.CIStr{
   470  					O: "uid",
   471  				},
   472  				FieldType: types.FieldType{
   473  					Flag: mysql.PriKeyFlag,
   474  				},
   475  			},
   476  		},
   477  		PKIsHandle: true,
   478  	}
   479  	info := model.WrapTableInfo(1, "", 0, &t)
   480  	cols := info.GetUniqueKeys()
   481  	c.Assert(cols, check.DeepEquals, [][]string{
   482  		{"uid"}, {"job"},
   483  	})
   484  }
   485  
   486  func (t *schemaSuite) TestMultiVersionStorage(c *check.C) {
   487  	defer testleak.AfterTest(c)()
   488  	ctx, cancel := context.WithCancel(context.Background())
   489  	dbName := timodel.NewCIStr("Test")
   490  	tbName := timodel.NewCIStr("T1")
   491  	// db and ignoreDB info
   492  	dbInfo := &timodel.DBInfo{
   493  		ID:    1,
   494  		Name:  dbName,
   495  		State: timodel.StatePublic,
   496  	}
   497  	var jobs []*timodel.Job
   498  	// `createSchema` job1
   499  	job := &timodel.Job{
   500  		ID:         3,
   501  		State:      timodel.JobStateSynced,
   502  		SchemaID:   1,
   503  		Type:       timodel.ActionCreateSchema,
   504  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 1, DBInfo: dbInfo, FinishedTS: 100},
   505  		Query:      "create database test",
   506  	}
   507  	jobs = append(jobs, job)
   508  
   509  	// table info
   510  	tblInfo := &timodel.TableInfo{
   511  		ID:    2,
   512  		Name:  tbName,
   513  		State: timodel.StatePublic,
   514  	}
   515  
   516  	// `createTable` job
   517  	job = &timodel.Job{
   518  		ID:         6,
   519  		State:      timodel.JobStateSynced,
   520  		SchemaID:   1,
   521  		TableID:    2,
   522  		Type:       timodel.ActionCreateTable,
   523  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 2, TableInfo: tblInfo, FinishedTS: 110},
   524  		Query:      "create table " + tbName.O,
   525  	}
   526  
   527  	jobs = append(jobs, job)
   528  
   529  	tbName = timodel.NewCIStr("T2")
   530  	// table info
   531  	tblInfo = &timodel.TableInfo{
   532  		ID:    3,
   533  		Name:  tbName,
   534  		State: timodel.StatePublic,
   535  	}
   536  	// `createTable` job
   537  	job = &timodel.Job{
   538  		ID:         6,
   539  		State:      timodel.JobStateSynced,
   540  		SchemaID:   1,
   541  		TableID:    3,
   542  		Type:       timodel.ActionCreateTable,
   543  		BinlogInfo: &timodel.HistoryInfo{SchemaVersion: 2, TableInfo: tblInfo, FinishedTS: 120},
   544  		Query:      "create table " + tbName.O,
   545  	}
   546  
   547  	jobs = append(jobs, job)
   548  	storage, err := NewSchemaStorage(nil, 0, nil, false)
   549  	c.Assert(err, check.IsNil)
   550  	for _, job := range jobs {
   551  		err := storage.HandleDDLJob(job)
   552  		c.Assert(err, check.IsNil)
   553  	}
   554  
   555  	// `dropTable` job
   556  	job = &timodel.Job{
   557  		ID:         6,
   558  		State:      timodel.JobStateSynced,
   559  		SchemaID:   1,
   560  		TableID:    2,
   561  		Type:       timodel.ActionDropTable,
   562  		BinlogInfo: &timodel.HistoryInfo{FinishedTS: 130},
   563  	}
   564  
   565  	err = storage.HandleDDLJob(job)
   566  	c.Assert(err, check.IsNil)
   567  
   568  	// `dropSchema` job
   569  	job = &timodel.Job{
   570  		ID:         6,
   571  		State:      timodel.JobStateSynced,
   572  		SchemaID:   1,
   573  		Type:       timodel.ActionDropSchema,
   574  		BinlogInfo: &timodel.HistoryInfo{FinishedTS: 140, DBInfo: dbInfo},
   575  	}
   576  
   577  	err = storage.HandleDDLJob(job)
   578  	c.Assert(err, check.IsNil)
   579  
   580  	c.Assert(storage.(*schemaStorageImpl).resolvedTs, check.Equals, uint64(140))
   581  	snap, err := storage.GetSnapshot(ctx, 100)
   582  	c.Assert(err, check.IsNil)
   583  	_, exist := snap.SchemaByID(1)
   584  	c.Assert(exist, check.IsTrue)
   585  	_, exist = snap.TableByID(2)
   586  	c.Assert(exist, check.IsFalse)
   587  	_, exist = snap.TableByID(3)
   588  	c.Assert(exist, check.IsFalse)
   589  
   590  	snap, err = storage.GetSnapshot(ctx, 115)
   591  	c.Assert(err, check.IsNil)
   592  	_, exist = snap.SchemaByID(1)
   593  	c.Assert(exist, check.IsTrue)
   594  	_, exist = snap.TableByID(2)
   595  	c.Assert(exist, check.IsTrue)
   596  	_, exist = snap.TableByID(3)
   597  	c.Assert(exist, check.IsFalse)
   598  
   599  	snap, err = storage.GetSnapshot(ctx, 125)
   600  	c.Assert(err, check.IsNil)
   601  	_, exist = snap.SchemaByID(1)
   602  	c.Assert(exist, check.IsTrue)
   603  	_, exist = snap.TableByID(2)
   604  	c.Assert(exist, check.IsTrue)
   605  	_, exist = snap.TableByID(3)
   606  	c.Assert(exist, check.IsTrue)
   607  
   608  	snap, err = storage.GetSnapshot(ctx, 135)
   609  	c.Assert(err, check.IsNil)
   610  	_, exist = snap.SchemaByID(1)
   611  	c.Assert(exist, check.IsTrue)
   612  	_, exist = snap.TableByID(2)
   613  	c.Assert(exist, check.IsFalse)
   614  	_, exist = snap.TableByID(3)
   615  	c.Assert(exist, check.IsTrue)
   616  
   617  	snap, err = storage.GetSnapshot(ctx, 140)
   618  	c.Assert(err, check.IsNil)
   619  	_, exist = snap.SchemaByID(1)
   620  	c.Assert(exist, check.IsFalse)
   621  	_, exist = snap.TableByID(2)
   622  	c.Assert(exist, check.IsFalse)
   623  	_, exist = snap.TableByID(3)
   624  	c.Assert(exist, check.IsFalse)
   625  
   626  	storage.DoGC(0)
   627  	snap, err = storage.GetSnapshot(ctx, 100)
   628  	c.Assert(err, check.IsNil)
   629  	_, exist = snap.SchemaByID(1)
   630  	c.Assert(exist, check.IsTrue)
   631  	_, exist = snap.TableByID(2)
   632  	c.Assert(exist, check.IsFalse)
   633  	_, exist = snap.TableByID(3)
   634  	c.Assert(exist, check.IsFalse)
   635  	storage.DoGC(115)
   636  	_, err = storage.GetSnapshot(ctx, 100)
   637  	c.Assert(err, check.NotNil)
   638  	snap, err = storage.GetSnapshot(ctx, 115)
   639  	c.Assert(err, check.IsNil)
   640  	_, exist = snap.SchemaByID(1)
   641  	c.Assert(exist, check.IsTrue)
   642  	_, exist = snap.TableByID(2)
   643  	c.Assert(exist, check.IsTrue)
   644  	_, exist = snap.TableByID(3)
   645  	c.Assert(exist, check.IsFalse)
   646  
   647  	storage.DoGC(155)
   648  	storage.AdvanceResolvedTs(185)
   649  
   650  	snap, err = storage.GetSnapshot(ctx, 180)
   651  	c.Assert(err, check.IsNil)
   652  	_, exist = snap.SchemaByID(1)
   653  	c.Assert(exist, check.IsFalse)
   654  	_, exist = snap.TableByID(2)
   655  	c.Assert(exist, check.IsFalse)
   656  	_, exist = snap.TableByID(3)
   657  	c.Assert(exist, check.IsFalse)
   658  	_, err = storage.GetSnapshot(ctx, 130)
   659  	c.Assert(err, check.NotNil)
   660  
   661  	cancel()
   662  	_, err = storage.GetSnapshot(ctx, 200)
   663  	c.Assert(errors.Cause(err), check.Equals, context.Canceled)
   664  }
   665  
   666  func (t *schemaSuite) TestCreateSnapFromMeta(c *check.C) {
   667  	defer testleak.AfterTest(c)()
   668  	store, err := mockstore.NewMockStore()
   669  	c.Assert(err, check.IsNil)
   670  	defer store.Close() //nolint:errcheck
   671  
   672  	session.SetSchemaLease(0)
   673  	session.DisableStats4Test()
   674  	domain, err := session.BootstrapSession(store)
   675  	c.Assert(err, check.IsNil)
   676  	defer domain.Close()
   677  	domain.SetStatsUpdating(true)
   678  	tk := testkit.NewTestKit(c, store)
   679  	tk.MustExec("create database test2")
   680  	tk.MustExec("create table test.simple_test1 (id bigint primary key)")
   681  	tk.MustExec("create table test.simple_test2 (id bigint primary key)")
   682  	tk.MustExec("create table test2.simple_test3 (id bigint primary key)")
   683  	tk.MustExec("create table test2.simple_test4 (id bigint primary key)")
   684  	tk.MustExec("create table test2.simple_test5 (a bigint)")
   685  	ver, err := store.CurrentVersion(oracle.GlobalTxnScope)
   686  	c.Assert(err, check.IsNil)
   687  	meta, err := kv.GetSnapshotMeta(store, ver.Ver)
   688  	c.Assert(err, check.IsNil)
   689  	snap, err := newSchemaSnapshotFromMeta(meta, ver.Ver, false)
   690  	c.Assert(err, check.IsNil)
   691  	_, ok := snap.GetTableByName("test", "simple_test1")
   692  	c.Assert(ok, check.IsTrue)
   693  	tableID, ok := snap.GetTableIDByName("test2", "simple_test5")
   694  	c.Assert(ok, check.IsTrue)
   695  	c.Assert(snap.IsIneligibleTableID(tableID), check.IsTrue)
   696  	dbInfo, ok := snap.SchemaByTableID(tableID)
   697  	c.Assert(ok, check.IsTrue)
   698  	c.Assert(dbInfo.Name.O, check.Equals, "test2")
   699  	c.Assert(len(snap.tableInSchema), check.Equals, 3)
   700  }
   701  
   702  func (t *schemaSuite) TestSnapshotClone(c *check.C) {
   703  	defer testleak.AfterTest(c)()
   704  	store, err := mockstore.NewMockStore()
   705  	c.Assert(err, check.IsNil)
   706  	defer store.Close() //nolint:errcheck
   707  
   708  	session.SetSchemaLease(0)
   709  	session.DisableStats4Test()
   710  	domain, err := session.BootstrapSession(store)
   711  	c.Assert(err, check.IsNil)
   712  	defer domain.Close()
   713  	domain.SetStatsUpdating(true)
   714  	tk := testkit.NewTestKit(c, store)
   715  	tk.MustExec("create database test2")
   716  	tk.MustExec("create table test.simple_test1 (id bigint primary key)")
   717  	tk.MustExec("create table test.simple_test2 (id bigint primary key)")
   718  	tk.MustExec("create table test2.simple_test3 (id bigint primary key)")
   719  	tk.MustExec("create table test2.simple_test4 (id bigint primary key)")
   720  	tk.MustExec("create table test2.simple_test5 (a bigint)")
   721  	ver, err := store.CurrentVersion(oracle.GlobalTxnScope)
   722  	c.Assert(err, check.IsNil)
   723  	meta, err := kv.GetSnapshotMeta(store, ver.Ver)
   724  	c.Assert(err, check.IsNil)
   725  	snap, err := newSchemaSnapshotFromMeta(meta, ver.Ver, false /* explicitTables */)
   726  	c.Assert(err, check.IsNil)
   727  
   728  	clone := snap.Clone()
   729  	c.Assert(clone.tableNameToID, check.DeepEquals, snap.tableNameToID)
   730  	c.Assert(clone.schemaNameToID, check.DeepEquals, snap.schemaNameToID)
   731  	c.Assert(clone.truncateTableID, check.DeepEquals, snap.truncateTableID)
   732  	c.Assert(clone.ineligibleTableID, check.DeepEquals, snap.ineligibleTableID)
   733  	c.Assert(clone.currentTs, check.Equals, snap.currentTs)
   734  	c.Assert(clone.explicitTables, check.Equals, snap.explicitTables)
   735  	c.Assert(len(clone.tables), check.Equals, len(snap.tables))
   736  	c.Assert(len(clone.schemas), check.Equals, len(snap.schemas))
   737  	c.Assert(len(clone.partitionTable), check.Equals, len(snap.partitionTable))
   738  
   739  	tableCount := len(snap.tables)
   740  	clone.tables = make(map[int64]*model.TableInfo)
   741  	c.Assert(len(snap.tables), check.Equals, tableCount)
   742  }
   743  
   744  func (t *schemaSuite) TestExplicitTables(c *check.C) {
   745  	defer testleak.AfterTest(c)()
   746  	store, err := mockstore.NewMockStore()
   747  	c.Assert(err, check.IsNil)
   748  	defer store.Close() //nolint:errcheck
   749  
   750  	session.SetSchemaLease(0)
   751  	session.DisableStats4Test()
   752  	domain, err := session.BootstrapSession(store)
   753  	c.Assert(err, check.IsNil)
   754  	defer domain.Close()
   755  	domain.SetStatsUpdating(true)
   756  	tk := testkit.NewTestKit(c, store)
   757  	ver1, err := store.CurrentVersion(oracle.GlobalTxnScope)
   758  	c.Assert(err, check.IsNil)
   759  	tk.MustExec("create database test2")
   760  	tk.MustExec("create table test.simple_test1 (id bigint primary key)")
   761  	tk.MustExec("create table test.simple_test2 (id bigint unique key)")
   762  	tk.MustExec("create table test2.simple_test3 (a bigint)")
   763  	tk.MustExec("create table test2.simple_test4 (a varchar(20) unique key)")
   764  	tk.MustExec("create table test2.simple_test5 (a varchar(20))")
   765  	ver2, err := store.CurrentVersion(oracle.GlobalTxnScope)
   766  	c.Assert(err, check.IsNil)
   767  	meta1, err := kv.GetSnapshotMeta(store, ver1.Ver)
   768  	c.Assert(err, check.IsNil)
   769  	snap1, err := newSchemaSnapshotFromMeta(meta1, ver1.Ver, true /* explicitTables */)
   770  	c.Assert(err, check.IsNil)
   771  	meta2, err := kv.GetSnapshotMeta(store, ver2.Ver)
   772  	c.Assert(err, check.IsNil)
   773  	snap2, err := newSchemaSnapshotFromMeta(meta2, ver2.Ver, false /* explicitTables */)
   774  	c.Assert(err, check.IsNil)
   775  	snap3, err := newSchemaSnapshotFromMeta(meta2, ver2.Ver, true /* explicitTables */)
   776  	c.Assert(err, check.IsNil)
   777  
   778  	c.Assert(len(snap2.tables)-len(snap1.tables), check.Equals, 5)
   779  	// some system tables are also ineligible
   780  	c.Assert(len(snap2.ineligibleTableID), check.GreaterEqual, 4)
   781  
   782  	c.Assert(len(snap3.tables)-len(snap1.tables), check.Equals, 5)
   783  	c.Assert(snap3.ineligibleTableID, check.HasLen, 0)
   784  }
   785  
   786  /*
   787  TODO: Untested Action:
   788  
   789  ActionAddForeignKey                 ActionType = 9
   790  ActionDropForeignKey                ActionType = 10
   791  ActionRebaseAutoID                  ActionType = 13
   792  ActionShardRowID                    ActionType = 16
   793  ActionLockTable                     ActionType = 27
   794  ActionUnlockTable                   ActionType = 28
   795  ActionRepairTable                   ActionType = 29
   796  ActionSetTiFlashReplica             ActionType = 30
   797  ActionUpdateTiFlashReplicaStatus    ActionType = 31
   798  ActionCreateSequence                ActionType = 34
   799  ActionAlterSequence                 ActionType = 35
   800  ActionDropSequence                  ActionType = 36
   801  ActionModifyTableAutoIdCache        ActionType = 39
   802  ActionRebaseAutoRandomBase          ActionType = 40
   803  ActionExchangeTablePartition        ActionType = 42
   804  ActionAddCheckConstraint            ActionType = 43
   805  ActionDropCheckConstraint           ActionType = 44
   806  ActionAlterCheckConstraint          ActionType = 45
   807  ActionAlterTableAlterPartition      ActionType = 46
   808  
   809  ... Any Action which of value is greater than 46 ...
   810  */
   811  func (t *schemaSuite) TestSchemaStorage(c *check.C) {
   812  	defer testleak.AfterTest(c)()
   813  	ctx := context.Background()
   814  	testCases := [][]string{{
   815  		"create database test_ddl1",                                                               // ActionCreateSchema
   816  		"create table test_ddl1.simple_test1 (id bigint primary key)",                             // ActionCreateTable
   817  		"create table test_ddl1.simple_test2 (id bigint)",                                         // ActionCreateTable
   818  		"create table test_ddl1.simple_test3 (id bigint primary key)",                             // ActionCreateTable
   819  		"create table test_ddl1.simple_test4 (id bigint primary key)",                             // ActionCreateTable
   820  		"DROP TABLE test_ddl1.simple_test3",                                                       // ActionDropTable
   821  		"ALTER TABLE test_ddl1.simple_test1 ADD COLUMN c1 INT NOT NULL",                           // ActionAddColumn
   822  		"ALTER TABLE test_ddl1.simple_test1 ADD c2 INT NOT NULL AFTER id",                         // ActionAddColumn
   823  		"ALTER TABLE test_ddl1.simple_test1 ADD c3 INT NOT NULL, ADD c4 INT NOT NULL",             // ActionAddColumns
   824  		"ALTER TABLE test_ddl1.simple_test1 DROP c1",                                              // ActionDropColumn
   825  		"ALTER TABLE test_ddl1.simple_test1 DROP c2, DROP c3",                                     // ActionDropColumns
   826  		"ALTER TABLE test_ddl1.simple_test1 ADD INDEX (c4)",                                       // ActionAddIndex
   827  		"ALTER TABLE test_ddl1.simple_test1 DROP INDEX c4",                                        // ActionDropIndex
   828  		"TRUNCATE test_ddl1.simple_test1",                                                         // ActionTruncateTable
   829  		"ALTER DATABASE test_ddl1 CHARACTER SET = binary COLLATE binary",                          // ActionModifySchemaCharsetAndCollate
   830  		"ALTER TABLE test_ddl1.simple_test2 ADD c1 INT NOT NULL, ADD c2 INT NOT NULL",             // ActionAddColumns
   831  		"ALTER TABLE test_ddl1.simple_test2 ADD INDEX (c1)",                                       // ActionAddIndex
   832  		"ALTER TABLE test_ddl1.simple_test2 ALTER INDEX c1 INVISIBLE",                             // ActionAlterIndexVisibility
   833  		"ALTER TABLE test_ddl1.simple_test2 RENAME INDEX c1 TO idx_c1",                            // ActionRenameIndex
   834  		"ALTER TABLE test_ddl1.simple_test2 MODIFY c2 BIGINT",                                     // ActionModifyColumn
   835  		"CREATE VIEW test_ddl1.view_test2 AS SELECT * FROM test_ddl1.simple_test2 WHERE id > 2",   // ActionCreateView
   836  		"DROP VIEW test_ddl1.view_test2",                                                          // ActionDropView
   837  		"RENAME TABLE test_ddl1.simple_test2 TO test_ddl1.simple_test5",                           // ActionRenameTable
   838  		"DROP DATABASE test_ddl1",                                                                 // ActionDropSchema
   839  		"create database test_ddl2",                                                               // ActionCreateSchema
   840  		"create table test_ddl2.simple_test1 (id bigint primary key, c1 int not null unique key)", // ActionCreateTable
   841  		`CREATE TABLE test_ddl2.employees  (
   842  			id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
   843  			fname VARCHAR(25) NOT NULL,
   844  			lname VARCHAR(25) NOT NULL,
   845  			store_id INT NOT NULL,
   846  			department_id INT NOT NULL
   847  		)
   848  
   849  		PARTITION BY RANGE(id)  (
   850  			PARTITION p0 VALUES LESS THAN (5),
   851  			PARTITION p1 VALUES LESS THAN (10),
   852  			PARTITION p2 VALUES LESS THAN (15),
   853  			PARTITION p3 VALUES LESS THAN (20)
   854  		)`, // ActionCreateTable
   855  		"ALTER TABLE test_ddl2.employees DROP PARTITION p2",                                  // ActionDropTablePartition
   856  		"ALTER TABLE test_ddl2.employees ADD PARTITION (PARTITION p4 VALUES LESS THAN (25))", // ActionAddTablePartition
   857  		"ALTER TABLE test_ddl2.employees TRUNCATE PARTITION p3",                              // ActionTruncateTablePartition
   858  		"alter table test_ddl2.employees comment='modify comment'",                           // ActionModifyTableComment
   859  		"alter table test_ddl2.simple_test1 drop primary key",                                // ActionDropPrimaryKey
   860  		"alter table test_ddl2.simple_test1 add primary key pk(id)",                          // ActionAddPrimaryKey
   861  		"ALTER TABLE test_ddl2.simple_test1 ALTER id SET DEFAULT 18",                         // ActionSetDefaultValue
   862  		"ALTER TABLE test_ddl2.simple_test1 CHARACTER SET = utf8mb4",                         // ActionModifyTableCharsetAndCollate
   863  		// "recover table test_ddl2.employees",                                                  // ActionRecoverTable this ddl can't work on mock tikv
   864  
   865  		"DROP TABLE test_ddl2.employees",
   866  		`CREATE TABLE test_ddl2.employees2  (
   867  			id INT NOT NULL,
   868  			fname VARCHAR(25) NOT NULL,
   869  			lname VARCHAR(25) NOT NULL,
   870  			store_id INT NOT NULL,
   871  			department_id INT NOT NULL
   872  		)
   873  
   874  		PARTITION BY RANGE(id)  (
   875  			PARTITION p0 VALUES LESS THAN (5),
   876  			PARTITION p1 VALUES LESS THAN (10),
   877  			PARTITION p2 VALUES LESS THAN (15),
   878  			PARTITION p3 VALUES LESS THAN (20)
   879  		)`,
   880  		"ALTER TABLE test_ddl2.employees2 CHARACTER SET = utf8mb4",
   881  		"DROP DATABASE test_ddl2",
   882  	}}
   883  
   884  	testOneGroup := func(tc []string) {
   885  		store, err := mockstore.NewMockStore()
   886  		c.Assert(err, check.IsNil)
   887  		defer store.Close() //nolint:errcheck
   888  		ticonfig.UpdateGlobal(func(conf *ticonfig.Config) {
   889  			conf.AlterPrimaryKey = true
   890  		})
   891  		session.SetSchemaLease(0)
   892  		session.DisableStats4Test()
   893  		domain, err := session.BootstrapSession(store)
   894  		c.Assert(err, check.IsNil)
   895  		defer domain.Close()
   896  		domain.SetStatsUpdating(true)
   897  		tk := testkit.NewTestKit(c, store)
   898  
   899  		for _, ddlSQL := range tc {
   900  			tk.MustExec(ddlSQL)
   901  		}
   902  
   903  		jobs, err := getAllHistoryDDLJob(store)
   904  		c.Assert(err, check.IsNil)
   905  		scheamStorage, err := NewSchemaStorage(nil, 0, nil, false)
   906  		c.Assert(err, check.IsNil)
   907  		for _, job := range jobs {
   908  			err := scheamStorage.HandleDDLJob(job)
   909  			c.Assert(err, check.IsNil)
   910  		}
   911  
   912  		for _, job := range jobs {
   913  			ts := job.BinlogInfo.FinishedTS
   914  			meta, err := kv.GetSnapshotMeta(store, ts)
   915  			c.Assert(err, check.IsNil)
   916  			snapFromMeta, err := newSchemaSnapshotFromMeta(meta, ts, false)
   917  			c.Assert(err, check.IsNil)
   918  			snapFromSchemaStore, err := scheamStorage.GetSnapshot(ctx, ts)
   919  			c.Assert(err, check.IsNil)
   920  
   921  			tidySchemaSnapshot(snapFromMeta)
   922  			tidySchemaSnapshot(snapFromSchemaStore)
   923  			c.Assert(snapFromMeta, check.DeepEquals, snapFromSchemaStore,
   924  				check.Commentf("%s", cmp.Diff(snapFromMeta, snapFromSchemaStore, cmp.AllowUnexported(schemaSnapshot{}, model.TableInfo{}))))
   925  		}
   926  	}
   927  
   928  	for _, tc := range testCases {
   929  		testOneGroup(tc)
   930  	}
   931  }
   932  
   933  func tidySchemaSnapshot(snap *schemaSnapshot) {
   934  	for _, dbInfo := range snap.schemas {
   935  		if len(dbInfo.Tables) == 0 {
   936  			dbInfo.Tables = nil
   937  		}
   938  	}
   939  	for _, tableInfo := range snap.tables {
   940  		tableInfo.TableInfoVersion = 0
   941  		if len(tableInfo.Columns) == 0 {
   942  			tableInfo.Columns = nil
   943  		}
   944  		if len(tableInfo.Indices) == 0 {
   945  			tableInfo.Indices = nil
   946  		}
   947  		if len(tableInfo.ForeignKeys) == 0 {
   948  			tableInfo.ForeignKeys = nil
   949  		}
   950  	}
   951  	// the snapshot from meta doesn't know which ineligible tables that have existed in history
   952  	// so we delete the ineligible tables which are already not exist
   953  	for tableID := range snap.ineligibleTableID {
   954  		if _, ok := snap.tables[tableID]; !ok {
   955  			delete(snap.ineligibleTableID, tableID)
   956  		}
   957  	}
   958  	// the snapshot from meta doesn't know which tables are truncated, so we just ignore it
   959  	snap.truncateTableID = nil
   960  	for _, v := range snap.tableInSchema {
   961  		sort.Slice(v, func(i, j int) bool { return v[i] < v[j] })
   962  	}
   963  }
   964  
   965  func getAllHistoryDDLJob(storage tidbkv.Storage) ([]*timodel.Job, error) {
   966  	s, err := session.CreateSession(storage)
   967  	if err != nil {
   968  		return nil, errors.Trace(err)
   969  	}
   970  
   971  	if s != nil {
   972  		defer s.Close()
   973  	}
   974  
   975  	store := domain.GetDomain(s.(sessionctx.Context)).Store()
   976  	txn, err := store.Begin()
   977  	if err != nil {
   978  		return nil, errors.Trace(err)
   979  	}
   980  	defer txn.Rollback() //nolint:errcheck
   981  	txnMeta := timeta.NewMeta(txn)
   982  
   983  	jobs, err := txnMeta.GetAllHistoryDDLJobs()
   984  	if err != nil {
   985  		return nil, errors.Trace(err)
   986  	}
   987  	return jobs, nil
   988  }