github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/backupccl/restore_mid_schema_change_test.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  package backupccl_test
    10  
    11  import (
    12  	"context"
    13  	"fmt"
    14  	"io/ioutil"
    15  	"os"
    16  	"path/filepath"
    17  	"strconv"
    18  	"testing"
    19  	"time"
    20  
    21  	"github.com/cockroachdb/cockroach/pkg/base"
    22  	"github.com/cockroachdb/cockroach/pkg/jobs"
    23  	"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
    24  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    25  	"github.com/cockroachdb/cockroach/pkg/util/log"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  // TestRestoreMidSchemaChanges attempts to RESTORE several BACKUPs that are
    30  // already constructed and store in
    31  // ccl/backupccl/testdata/restore_mid_schema_change. These backups were taken on
    32  // tables that were in the process of performing a schema change. In particular,
    33  // the schema changes were temporarily blocked after it completed its backfill
    34  // stage.
    35  //
    36  // This test ensures that these BACKUPS can be:
    37  // 1) Restore
    38  // 2) Can read the data from the restored table
    39  // 3) The schema changes that were in progress on the table complete and are
    40  // applied.
    41  // 4) Can apply new schema changes. (This is used to ensure that there are no
    42  // more mutations hanging on the table descriptor.)
    43  //
    44  // The test cases are organized based on cluster version of the cluster that
    45  // took the BACKUP. Each test-case represents a BACKUP. They were created using
    46  // the statements provided in the create.sql file in the appropriate testdata
    47  // dir. All backups backed up defaultdb.*, which contain the relevant tables.
    48  // Most backups contain a single table whose name matches the backup name. If
    49  // the backup is expected to contain several tables, the table names will be
    50  // backupName1, backupName2, ...
    51  func TestRestoreMidSchemaChange(t *testing.T) {
    52  	defer leaktest.AfterTest(t)()
    53  	const (
    54  		testdataBase = "testdata/restore_mid_schema_change"
    55  		exportDirs   = testdataBase + "/exports"
    56  	)
    57  	versionDirs, err := ioutil.ReadDir(exportDirs)
    58  	require.NoError(t, err)
    59  	for _, clusterVersionDir := range versionDirs {
    60  		require.True(t, clusterVersionDir.IsDir())
    61  		fullClusterVersionDir, err := filepath.Abs(filepath.Join(exportDirs, clusterVersionDir.Name()))
    62  		require.NoError(t, err)
    63  		backupDirs, err := ioutil.ReadDir(fullClusterVersionDir)
    64  		require.NoError(t, err)
    65  		// In each version folder (e.g. "19.2", "20.1"), there is a backup for each schema change.
    66  		for _, backupDir := range backupDirs {
    67  			fullBackupDir, err := filepath.Abs(filepath.Join(fullClusterVersionDir, backupDir.Name()))
    68  			require.NoError(t, err)
    69  			t.Run(clusterVersionDir.Name()+"-"+backupDir.Name(), restoreMidSchemaChange(fullBackupDir, backupDir.Name()))
    70  		}
    71  	}
    72  }
    73  
    74  func restoreMidSchemaChange(backupDir, schemaChangeName string) func(t *testing.T) {
    75  	verify := func(t *testing.T, scName string, sqlDB *sqlutils.SQLRunner) {
    76  		var expectedData [][]string
    77  		tableName := fmt.Sprintf("defaultdb.%s", scName)
    78  		sqlDB.CheckQueryResultsRetry(t, "SELECT * FROM crdb_internal.jobs WHERE job_type = 'SCHEMA CHANGE' AND status <> 'succeeded'", [][]string{})
    79  		numSchemaChangeJobs := 1
    80  		// This enumerates the tests cases and specifies how each case should be
    81  		// handled.
    82  		switch scName {
    83  		case "midaddcol":
    84  			expectedData = [][]string{{"1", "1.3"}, {"2", "1.3"}, {"3", "1.3"}}
    85  		case "midaddconst":
    86  			expectedData = [][]string{{"1"}, {"2"}, {"3"}}
    87  			sqlDB.CheckQueryResults(t, "SELECT count(*) FROM [SHOW CONSTRAINTS FROM defaultdb.midaddconst] WHERE constraint_name = 'my_const'", [][]string{{"1"}})
    88  		case "midaddindex":
    89  			expectedData = [][]string{{"1"}, {"2"}, {"3"}}
    90  			sqlDB.CheckQueryResults(t, "SELECT count(*) FROM [SHOW INDEXES FROM defaultdb.midaddindex] WHERE column_name = 'a'", [][]string{{"1"}})
    91  		case "middropcol":
    92  			expectedData = [][]string{{"1"}, {"1"}, {"1"}, {"2"}, {"2"}, {"2"}, {"3"}, {"3"}, {"3"}}
    93  		case "midmany":
    94  			numSchemaChangeJobs = 3
    95  			expectedData = [][]string{{"1", "1.3"}, {"2", "1.3"}, {"3", "1.3"}}
    96  			sqlDB.CheckQueryResults(t, "SELECT count(*) FROM [SHOW CONSTRAINTS FROM defaultdb.midmany] WHERE constraint_name = 'my_const'", [][]string{{"1"}})
    97  			sqlDB.CheckQueryResults(t, "SELECT count(*) FROM [SHOW INDEXES FROM defaultdb.midmany] WHERE column_name = 'a'", [][]string{{"1"}})
    98  		case "midmultitxn":
    99  			expectedData = [][]string{{"1", "1.3"}, {"2", "1.3"}, {"3", "1.3"}}
   100  			sqlDB.CheckQueryResults(t, "SELECT count(*) FROM [SHOW CONSTRAINTS FROM defaultdb.midmultitxn] WHERE constraint_name = 'my_const'", [][]string{{"1"}})
   101  			sqlDB.CheckQueryResults(t, "SELECT count(*) FROM [SHOW INDEXES FROM defaultdb.midmultitxn] WHERE column_name = 'a'", [][]string{{"1"}})
   102  		case "midmultitable":
   103  			numSchemaChangeJobs = 2
   104  			expectedData = [][]string{{"1", "1.3"}, {"2", "1.3"}, {"3", "1.3"}}
   105  			sqlDB.CheckQueryResults(t, fmt.Sprintf("SELECT * FROM %s1", tableName), expectedData)
   106  			expectedData = [][]string{{"1"}, {"2"}, {"3"}}
   107  			sqlDB.CheckQueryResults(t, fmt.Sprintf("SELECT * FROM %s2", tableName), expectedData)
   108  			tableName += "1"
   109  		case "midprimarykeyswap":
   110  			// The primary key swap will also create a cleanup job.
   111  			numSchemaChangeJobs = 2
   112  			expectedData = [][]string{{"1"}, {"2"}, {"3"}}
   113  		case "midprimarykeyswapcleanup":
   114  			// This backup only contains the cleanup job mentioned above.
   115  			expectedData = [][]string{{"1"}, {"2"}, {"3"}}
   116  		}
   117  		if scName != "midmultitable" {
   118  			sqlDB.CheckQueryResults(t, fmt.Sprintf("SELECT * FROM %s", tableName), expectedData)
   119  		}
   120  		sqlDB.CheckQueryResults(t, "SELECT count(*) FROM crdb_internal.jobs WHERE job_type = 'SCHEMA CHANGE'", [][]string{{strconv.Itoa(numSchemaChangeJobs)}})
   121  		// Ensure that a schema change can complete on the restored table.
   122  		schemaChangeQuery := fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT post_restore_const CHECK (a > 0)", tableName)
   123  		sqlDB.Exec(t, schemaChangeQuery)
   124  	}
   125  
   126  	return func(t *testing.T) {
   127  		params := base.TestServerArgs{}
   128  		defer func(oldInterval time.Duration) {
   129  			jobs.DefaultAdoptInterval = oldInterval
   130  		}(jobs.DefaultAdoptInterval)
   131  		jobs.DefaultAdoptInterval = 100 * time.Millisecond
   132  		const numAccounts = 1000
   133  		_, _, sqlDB, dir, cleanup := backupRestoreTestSetupWithParams(t, singleNode, numAccounts,
   134  			initNone, base.TestClusterArgs{ServerArgs: params})
   135  		defer cleanup()
   136  		symlink := filepath.Join(dir, "foo")
   137  		err := os.Symlink(backupDir, symlink)
   138  		require.NoError(t, err)
   139  		sqlDB.Exec(t, "USE defaultdb")
   140  		restoreQuery := fmt.Sprintf("RESTORE defaultdb.* from $1")
   141  		log.Infof(context.Background(), "%+v", sqlDB.QueryStr(t, "SHOW BACKUP $1", localFoo))
   142  		sqlDB.Exec(t, restoreQuery, localFoo)
   143  		verify(t, schemaChangeName, sqlDB)
   144  	}
   145  }