github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/scrub_test.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package sql_test
    12  
    13  import (
    14  	"context"
    15  	gosql "database/sql"
    16  	"fmt"
    17  	"regexp"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/cockroachdb/cockroach/pkg/base"
    23  	"github.com/cockroachdb/cockroach/pkg/keys"
    24  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/scrub"
    26  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    27  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    28  	"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
    29  	"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
    30  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    31  )
    32  
    33  // TestScrubIndexMissingIndexEntry tests that
    34  // `SCRUB TABLE ... INDEX ALL`` will find missing index entries. To test
    35  // this, a row's underlying secondary index k/v is deleted using the KV
    36  // client. This causes a missing index entry error as the row is missing
    37  // the expected secondary index k/v.
    38  func TestScrubIndexMissingIndexEntry(t *testing.T) {
    39  	defer leaktest.AfterTest(t)()
    40  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
    41  	defer s.Stopper().Stop(context.Background())
    42  	r := sqlutils.MakeSQLRunner(db)
    43  
    44  	// Create the table and the row entry.
    45  	// We use a table with mixed as a regression case for #38184.
    46  	if _, err := db.Exec(`
    47  CREATE DATABASE t;
    48  CREATE TABLE t."tEst" ("K" INT PRIMARY KEY, v INT);
    49  CREATE INDEX secondary ON t."tEst" (v);
    50  INSERT INTO t."tEst" VALUES (10, 20);
    51  `); err != nil {
    52  		t.Fatalf("unexpected error: %s", err)
    53  	}
    54  
    55  	// Construct datums for our row values (k, v).
    56  	values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(20)}
    57  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "tEst")
    58  	secondaryIndex := &tableDesc.Indexes[0]
    59  
    60  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
    61  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
    62  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
    63  
    64  	// Construct the secondary index key that is currently in the
    65  	// database.
    66  	secondaryIndexKey, err := sqlbase.EncodeSecondaryIndex(
    67  		keys.SystemSQLCodec, tableDesc, secondaryIndex, colIDtoRowIndex, values, true /* includeEmpty */)
    68  	if err != nil {
    69  		t.Fatalf("unexpected error: %s", err)
    70  	}
    71  
    72  	if len(secondaryIndexKey) != 1 {
    73  		t.Fatalf("expected 1 index entry, got %d. got %#v", len(secondaryIndexKey), secondaryIndexKey)
    74  	}
    75  
    76  	// Delete the entry.
    77  	if err := kvDB.Del(context.Background(), secondaryIndexKey[0].Key); err != nil {
    78  		t.Fatalf("unexpected error: %s", err)
    79  	}
    80  
    81  	// Run SCRUB and find the index errors we created.
    82  	exp := expectedScrubResult{
    83  		ErrorType:    scrub.MissingIndexEntryError,
    84  		Database:     "t",
    85  		Table:        "tEst",
    86  		PrimaryKey:   "(10)",
    87  		Repaired:     false,
    88  		DetailsRegex: `"v": "20"`,
    89  	}
    90  	runScrub(t, db, `EXPERIMENTAL SCRUB TABLE t."tEst" WITH OPTIONS INDEX ALL`, exp)
    91  	// Run again with AS OF SYSTEM TIME.
    92  	time.Sleep(1 * time.Millisecond)
    93  	runScrub(t, db, `EXPERIMENTAL SCRUB TABLE t."tEst" AS OF SYSTEM TIME '-1ms' WITH OPTIONS INDEX ALL`, exp)
    94  
    95  	// Verify that AS OF SYSTEM TIME actually operates in the past.
    96  	ts := r.QueryStr(t, `SELECT cluster_logical_timestamp()`)[0][0]
    97  	r.Exec(t, `DELETE FROM t."tEst"`)
    98  	runScrub(
    99  		t, db, fmt.Sprintf(
   100  			`EXPERIMENTAL SCRUB TABLE t."tEst" AS OF SYSTEM TIME '%s' WITH OPTIONS INDEX ALL`, ts,
   101  		),
   102  		exp,
   103  	)
   104  }
   105  
   106  // TestScrubIndexDanglingIndexReference tests that
   107  // `SCRUB TABLE ... INDEX`` will find dangling index references, which
   108  // are index entries that have no corresponding primary k/v. To test
   109  // this an index entry is generated and inserted. This creates a
   110  // dangling index error as the corresponding primary k/v is not equal.
   111  func TestScrubIndexDanglingIndexReference(t *testing.T) {
   112  	defer leaktest.AfterTest(t)()
   113  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   114  	defer s.Stopper().Stop(context.Background())
   115  
   116  	// Create the table and the row entry.
   117  	if _, err := db.Exec(`
   118  CREATE DATABASE t;
   119  CREATE TABLE t.test (k INT PRIMARY KEY, v INT);
   120  CREATE INDEX secondary ON t.test (v);
   121  `); err != nil {
   122  		t.Fatalf("unexpected error: %s", err)
   123  	}
   124  
   125  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   126  	secondaryIndexDesc := &tableDesc.Indexes[0]
   127  
   128  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   129  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   130  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   131  
   132  	// Construct datums and secondary k/v for our row values (k, v).
   133  	values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(314)}
   134  	secondaryIndex, err := sqlbase.EncodeSecondaryIndex(
   135  		keys.SystemSQLCodec, tableDesc, secondaryIndexDesc, colIDtoRowIndex, values, true /* includeEmpty */)
   136  	if err != nil {
   137  		t.Fatalf("unexpected error: %s", err)
   138  	}
   139  
   140  	if len(secondaryIndex) != 1 {
   141  		t.Fatalf("expected 1 index entry, got %d. got %#v", len(secondaryIndex), secondaryIndex)
   142  	}
   143  
   144  	// Put the new secondary k/v into the database.
   145  	if err := kvDB.Put(context.Background(), secondaryIndex[0].Key, &secondaryIndex[0].Value); err != nil {
   146  		t.Fatalf("unexpected error: %s", err)
   147  	}
   148  
   149  	// Run SCRUB and find the index errors we created.
   150  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS INDEX ALL`)
   151  	if err != nil {
   152  		t.Fatalf("unexpected error: %s", err)
   153  	}
   154  	defer rows.Close()
   155  
   156  	results, err := sqlutils.GetScrubResultRows(rows)
   157  	if err != nil {
   158  		t.Fatalf("unexpected error: %s", err)
   159  	}
   160  
   161  	if len(results) != 1 {
   162  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   163  	}
   164  	if result := results[0]; result.ErrorType != scrub.DanglingIndexReferenceError {
   165  		t.Fatalf("expected %q error, instead got: %s",
   166  			scrub.DanglingIndexReferenceError, result.ErrorType)
   167  	} else if result.Database != "t" {
   168  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   169  	} else if result.Table != "test" {
   170  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   171  	} else if result.PrimaryKey != "(10)" {
   172  		t.Fatalf("expected primaryKey %q, got %q", "(10)", result.PrimaryKey)
   173  	} else if result.Repaired {
   174  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   175  	} else if !strings.Contains(result.Details, `"v": "314"`) {
   176  		t.Fatalf("expected error details to contain `%s`, got %s", `"v": "314"`, result.Details)
   177  	}
   178  
   179  	// Run SCRUB DATABASE to make sure it also catches the problem.
   180  	rows, err = db.Query(`EXPERIMENTAL SCRUB DATABASE t`)
   181  	if err != nil {
   182  		t.Fatalf("unexpected error: %+v", err)
   183  	}
   184  	defer rows.Close()
   185  	scrubDatabaseResults, err := sqlutils.GetScrubResultRows(rows)
   186  	if err != nil {
   187  		t.Fatalf("unexpected error: %s", err)
   188  	} else if len(scrubDatabaseResults) != 1 {
   189  		t.Fatalf("expected 1 result, got %d. got %#v", len(scrubDatabaseResults), scrubDatabaseResults)
   190  	} else if !(scrubDatabaseResults[0].ErrorType == results[0].ErrorType &&
   191  		scrubDatabaseResults[0].Database == results[0].Database &&
   192  		scrubDatabaseResults[0].Table == results[0].Table &&
   193  		scrubDatabaseResults[0].Details == results[0].Details) {
   194  		t.Fatalf("expected results to be equal, SCRUB TABLE got %v. SCRUB DATABASE got %v",
   195  			results, scrubDatabaseResults)
   196  	}
   197  }
   198  
   199  // TestScrubIndexCatchesStoringMismatch tests that
   200  // `SCRUB TABLE ... INDEX ALL` will fail if an index entry only differs
   201  // by its STORING values. To test this, a row's underlying secondary
   202  // index k/v is updated using the KV client to have a different value.
   203  func TestScrubIndexCatchesStoringMismatch(t *testing.T) {
   204  	defer leaktest.AfterTest(t)()
   205  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   206  	defer s.Stopper().Stop(context.Background())
   207  
   208  	// Create the table and the row entry.
   209  	if _, err := db.Exec(`
   210  CREATE DATABASE t;
   211  CREATE TABLE t.test (k INT PRIMARY KEY, v INT, data INT);
   212  CREATE INDEX secondary ON t.test (v) STORING (data);
   213  INSERT INTO t.test VALUES (10, 20, 1337);
   214  `); err != nil {
   215  		t.Fatalf("unexpected error: %s", err)
   216  	}
   217  
   218  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   219  	secondaryIndexDesc := &tableDesc.Indexes[0]
   220  
   221  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   222  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   223  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   224  	colIDtoRowIndex[tableDesc.Columns[2].ID] = 2
   225  
   226  	// Generate the existing secondary index key.
   227  	values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(20), tree.NewDInt(1337)}
   228  	secondaryIndex, err := sqlbase.EncodeSecondaryIndex(
   229  		keys.SystemSQLCodec, tableDesc, secondaryIndexDesc, colIDtoRowIndex, values, true /* includeEmpty */)
   230  
   231  	if len(secondaryIndex) != 1 {
   232  		t.Fatalf("expected 1 index entry, got %d. got %#v", len(secondaryIndex), secondaryIndex)
   233  	}
   234  
   235  	if err != nil {
   236  		t.Fatalf("unexpected error: %s", err)
   237  	}
   238  	// Delete the existing secondary k/v.
   239  	if err := kvDB.Del(context.Background(), secondaryIndex[0].Key); err != nil {
   240  		t.Fatalf("unexpected error: %s", err)
   241  	}
   242  
   243  	// Generate a secondary index k/v that has a different value.
   244  	values = []tree.Datum{tree.NewDInt(10), tree.NewDInt(20), tree.NewDInt(314)}
   245  	secondaryIndex, err = sqlbase.EncodeSecondaryIndex(
   246  		keys.SystemSQLCodec, tableDesc, secondaryIndexDesc, colIDtoRowIndex, values, true /* includeEmpty */)
   247  	if err != nil {
   248  		t.Fatalf("unexpected error: %s", err)
   249  	}
   250  	// Put the incorrect secondary k/v.
   251  	if err := kvDB.Put(context.Background(), secondaryIndex[0].Key, &secondaryIndex[0].Value); err != nil {
   252  		t.Fatalf("unexpected error: %s", err)
   253  	}
   254  
   255  	// Run SCRUB and find the index errors we created.
   256  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS INDEX ALL`)
   257  	if err != nil {
   258  		t.Fatalf("unexpected error: %s", err)
   259  	}
   260  	defer rows.Close()
   261  
   262  	results, err := sqlutils.GetScrubResultRows(rows)
   263  	if err != nil {
   264  		t.Fatalf("unexpected error: %s", err)
   265  	}
   266  
   267  	// We will receive both a missing_index_entry and dangling_index_reference.
   268  	if len(results) != 2 {
   269  		t.Fatalf("expected 2 result, got %d. got %#v", len(results), results)
   270  	}
   271  
   272  	// Assert the missing index error is correct.
   273  	var missingIndexError *sqlutils.ScrubResult
   274  	for _, result := range results {
   275  		if result.ErrorType == scrub.MissingIndexEntryError {
   276  			missingIndexError = &result
   277  			break
   278  		}
   279  	}
   280  	if result := missingIndexError; result == nil {
   281  		t.Fatalf("expected errors to include %q error, but got errors: %#v",
   282  			scrub.MissingIndexEntryError, results)
   283  	} else if result.Database != "t" {
   284  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   285  	} else if result.Table != "test" {
   286  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   287  	} else if result.PrimaryKey != "(10)" {
   288  		t.Fatalf("expected primaryKey %q, got %q", "(10)", result.PrimaryKey)
   289  	} else if result.Repaired {
   290  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   291  	} else if !strings.Contains(result.Details, `"data": "1337"`) {
   292  		t.Fatalf("expected error details to contain `%s`, got %s", `"data": "1337"`, result.Details)
   293  	}
   294  
   295  	// Assert the dangling index error is correct.
   296  	var danglingIndexResult *sqlutils.ScrubResult
   297  	for _, result := range results {
   298  		if result.ErrorType == scrub.DanglingIndexReferenceError {
   299  			danglingIndexResult = &result
   300  			break
   301  		}
   302  	}
   303  	if result := danglingIndexResult; result == nil {
   304  		t.Fatalf("expected errors to include %q error, but got errors: %#v",
   305  			scrub.DanglingIndexReferenceError, results)
   306  	} else if result.Database != "t" {
   307  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   308  	} else if result.Table != "test" {
   309  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   310  	} else if result.PrimaryKey != "(10)" {
   311  		t.Fatalf("expected primaryKey %q, got %q", "(10)", result.PrimaryKey)
   312  	} else if result.Repaired {
   313  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   314  	} else if !strings.Contains(result.Details, `"data": "314"`) {
   315  		t.Fatalf("expected error details to contain `%s`, got %s", `"data": "314"`, result.Details)
   316  	}
   317  }
   318  
   319  // TestScrubCheckConstraint tests that `SCRUB TABLE ... CONSTRAINT ALL`
   320  // will fail if a check constraint is violated. To test this, a row's
   321  // underlying value is updated using the KV client so the row violates
   322  // the constraint..
   323  func TestScrubCheckConstraint(t *testing.T) {
   324  	defer leaktest.AfterTest(t)()
   325  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   326  	defer s.Stopper().Stop(context.Background())
   327  
   328  	// Create the table and the row entry.
   329  	if _, err := db.Exec(`
   330  CREATE DATABASE t;
   331  CREATE TABLE t.test (k INT PRIMARY KEY, v INT, CHECK (v > 1));
   332  INSERT INTO t.test VALUES (10, 2);
   333  `); err != nil {
   334  		t.Fatalf("unexpected error: %s", err)
   335  	}
   336  
   337  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   338  
   339  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   340  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   341  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   342  
   343  	// Create the primary index key.
   344  	values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(2)}
   345  	primaryIndexKeyPrefix := sqlbase.MakeIndexKeyPrefix(
   346  		keys.SystemSQLCodec, tableDesc, tableDesc.PrimaryIndex.ID)
   347  	primaryIndexKey, _, err := sqlbase.EncodeIndexKey(
   348  		tableDesc, &tableDesc.PrimaryIndex, colIDtoRowIndex, values, primaryIndexKeyPrefix)
   349  	if err != nil {
   350  		t.Fatalf("unexpected error: %s", err)
   351  	}
   352  
   353  	// Add the family suffix to the key.
   354  	family := tableDesc.Families[0]
   355  	primaryIndexKey = keys.MakeFamilyKey(primaryIndexKey, uint32(family.ID))
   356  
   357  	// Generate a k/v that has a different value that violates the
   358  	// constraint.
   359  	values = []tree.Datum{tree.NewDInt(10), tree.NewDInt(0)}
   360  	// Encode the column value.
   361  	valueBuf, err := sqlbase.EncodeTableValue(
   362  		[]byte(nil), tableDesc.Columns[1].ID, values[1], []byte(nil))
   363  	if err != nil {
   364  		t.Fatalf("unexpected error: %s", err)
   365  	}
   366  	// Construct the tuple for the family value.
   367  	var value roachpb.Value
   368  	value.SetTuple(valueBuf)
   369  
   370  	// Overwrite the existing value.
   371  	if err := kvDB.Put(context.Background(), primaryIndexKey, &value); err != nil {
   372  		t.Fatalf("unexpected error: %s", err)
   373  	}
   374  	// Run SCRUB and find the CHECK violation created.
   375  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS CONSTRAINT ALL`)
   376  	if err != nil {
   377  		t.Fatalf("unexpected error: %s", err)
   378  	}
   379  	defer rows.Close()
   380  	results, err := sqlutils.GetScrubResultRows(rows)
   381  	if err != nil {
   382  		t.Fatalf("unexpected error: %s", err)
   383  	}
   384  
   385  	if len(results) != 1 {
   386  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   387  	}
   388  
   389  	if result := results[0]; result.ErrorType != string(scrub.CheckConstraintViolation) {
   390  		t.Fatalf("expected %q error, instead got: %s",
   391  			scrub.CheckConstraintViolation, result.ErrorType)
   392  	} else if result.Database != "t" {
   393  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   394  	} else if result.Table != "test" {
   395  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   396  	} else if result.PrimaryKey != "(10)" {
   397  		t.Fatalf("expected primaryKey %q, got %q", "(10)", result.PrimaryKey)
   398  	} else if result.Repaired {
   399  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   400  	} else if !strings.Contains(result.Details,
   401  		`{"constraint_name": "check_v", "row_data": {"k": "10", "v": "0"}}`) {
   402  		t.Fatalf("expected error details to contain `%s`, got %s",
   403  			`{"constraint_name": "check_v", "row_data": {"k": "10", "v": "0"}}`,
   404  			result.Details)
   405  	}
   406  }
   407  
   408  // TestScrubFKConstraintFKMissing tests that `SCRUB TABLE ... CONSTRAINT
   409  // ALL` will report an error when a foreign key constraint is violated.
   410  // To test this, the secondary index used for the foreign key lookup is
   411  // modified using the KV client to change the value and cause a
   412  // violation.
   413  func TestScrubFKConstraintFKMissing(t *testing.T) {
   414  	defer leaktest.AfterTest(t)()
   415  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   416  	defer s.Stopper().Stop(context.Background())
   417  	r := sqlutils.MakeSQLRunner(db)
   418  
   419  	// Create the table and the row entry.
   420  	r.Exec(t, `
   421  		CREATE DATABASE t;
   422  		CREATE TABLE t.parent (
   423  			id INT PRIMARY KEY
   424  		);
   425  		CREATE TABLE t.child (
   426  			child_id INT PRIMARY KEY,
   427  			parent_id INT,
   428  			INDEX (parent_id),
   429  			FOREIGN KEY (parent_id) REFERENCES t.parent (id)
   430  		);
   431  		INSERT INTO t.parent VALUES (314);
   432  		INSERT INTO t.child VALUES (10, 314);
   433  	`)
   434  
   435  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "child")
   436  
   437  	// Construct datums for the child row values (child_id, parent_id).
   438  	values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(314)}
   439  	secondaryIndex := &tableDesc.Indexes[0]
   440  
   441  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   442  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   443  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   444  
   445  	// Construct the secondary index key entry as it exists in the
   446  	// database.
   447  	secondaryIndexKey, err := sqlbase.EncodeSecondaryIndex(
   448  		keys.SystemSQLCodec, tableDesc, secondaryIndex, colIDtoRowIndex, values, true /* includeEmpty */)
   449  	if err != nil {
   450  		t.Fatalf("unexpected error: %s", err)
   451  	}
   452  
   453  	if len(secondaryIndexKey) != 1 {
   454  		t.Fatalf("expected 1 index entry, got %d. got %#v", len(secondaryIndexKey), secondaryIndexKey)
   455  	}
   456  
   457  	// Delete the existing secondary key entry, as we will later replace
   458  	// it.
   459  	if err := kvDB.Del(context.Background(), secondaryIndexKey[0].Key); err != nil {
   460  		t.Fatalf("unexpected error: %s", err)
   461  	}
   462  
   463  	// Replace the foreign key value.
   464  	values[1] = tree.NewDInt(0)
   465  
   466  	// Construct the new secondary index key that will be inserted.
   467  	secondaryIndexKey, err = sqlbase.EncodeSecondaryIndex(
   468  		keys.SystemSQLCodec, tableDesc, secondaryIndex, colIDtoRowIndex, values, true /* includeEmpty */)
   469  	if err != nil {
   470  		t.Fatalf("unexpected error: %s", err)
   471  	}
   472  
   473  	if len(secondaryIndexKey) != 1 {
   474  		t.Fatalf("expected 1 index entry, got %d. got %#v", len(secondaryIndexKey), secondaryIndexKey)
   475  	}
   476  
   477  	// Add the new, replacement secondary index entry.
   478  	if err := kvDB.Put(context.Background(), secondaryIndexKey[0].Key, &secondaryIndexKey[0].Value); err != nil {
   479  		t.Fatalf("unexpected error: %s", err)
   480  	}
   481  
   482  	// Run SCRUB and find the FOREIGN KEY violation created.
   483  	exp := expectedScrubResult{
   484  		ErrorType:    scrub.ForeignKeyConstraintViolation,
   485  		Database:     "t",
   486  		Table:        "child",
   487  		PrimaryKey:   "(10)",
   488  		DetailsRegex: `{"constraint_name": "fk_parent_id_ref_parent", "row_data": {"child_id": "10", "parent_id": "0"}}`,
   489  	}
   490  	runScrub(t, db, `EXPERIMENTAL SCRUB TABLE t.child WITH OPTIONS CONSTRAINT ALL`, exp)
   491  	// Run again with AS OF SYSTEM TIME.
   492  	time.Sleep(1 * time.Millisecond)
   493  	runScrub(t, db, `EXPERIMENTAL SCRUB TABLE t.child AS OF SYSTEM TIME '-1ms' WITH OPTIONS CONSTRAINT ALL`, exp)
   494  
   495  	// Verify that AS OF SYSTEM TIME actually operates in the past.
   496  	ts := r.QueryStr(t, `SELECT cluster_logical_timestamp()`)[0][0]
   497  	r.Exec(t, "INSERT INTO t.parent VALUES (0)")
   498  	runScrub(
   499  		t, db, fmt.Sprintf(
   500  			`EXPERIMENTAL SCRUB TABLE t.child AS OF SYSTEM TIME '%s' WITH OPTIONS CONSTRAINT ALL`, ts,
   501  		),
   502  		exp,
   503  	)
   504  }
   505  
   506  // TestScrubFKConstraintFKNulls tests that `SCRUB TABLE ... CONSTRAINT ALL` will
   507  // fail if a MATCH FULL foreign key constraint is violated when foreign key
   508  // values are partially null.
   509  // TODO (lucy): This is making use of the fact that SCRUB reports errors for
   510  // unvalidated FKs, even when it's fine for rows to violate the constraint.
   511  // Ideally we would have SCRUB not report errors for those, and use a validated
   512  // constraint in this test with corrupted KVs.
   513  func TestScrubFKConstraintFKNulls(t *testing.T) {
   514  	defer leaktest.AfterTest(t)()
   515  	s, db, _ := serverutils.StartServer(t, base.TestServerArgs{})
   516  	defer s.Stopper().Stop(context.Background())
   517  
   518  	// Create the table and the row entry.
   519  	if _, err := db.Exec(`
   520  CREATE DATABASE t;
   521  CREATE TABLE t.parent (
   522  	id INT PRIMARY KEY,
   523  	id2 INT,
   524  	UNIQUE INDEX (id, id2)
   525  );
   526  CREATE TABLE t.child (
   527  	child_id INT PRIMARY KEY,
   528  	parent_id INT,
   529  	parent_id2 INT,
   530  	INDEX (parent_id, parent_id2)
   531  );
   532  INSERT INTO t.parent VALUES (1337, NULL);
   533  INSERT INTO t.child VALUES (11, 1337, NULL);
   534  ALTER TABLE t.child ADD FOREIGN KEY (parent_id, parent_id2) REFERENCES t.parent (id, id2) MATCH FULL NOT VALID;
   535  `); err != nil {
   536  		t.Fatalf("unexpected error: %s", err)
   537  	}
   538  
   539  	// Run SCRUB and find the FOREIGN KEY violation created.
   540  	exp := expectedScrubResult{
   541  		ErrorType:    scrub.ForeignKeyConstraintViolation,
   542  		Database:     "t",
   543  		Table:        "child",
   544  		PrimaryKey:   "(11)",
   545  		DetailsRegex: `{"constraint_name": "fk_parent_id_ref_parent", "row_data": {"child_id": "11", "parent_id": "1337", "parent_id2": "NULL"}}`,
   546  	}
   547  	runScrub(t, db, `EXPERIMENTAL SCRUB TABLE t.child WITH OPTIONS CONSTRAINT ALL`, exp)
   548  	time.Sleep(1 * time.Millisecond)
   549  	runScrub(t, db, `EXPERIMENTAL SCRUB TABLE t.child AS OF SYSTEM TIME '-1ms' WITH OPTIONS CONSTRAINT ALL`, exp)
   550  }
   551  
   552  // TestScrubPhysicalNonnullableNullInSingleColumnFamily tests that
   553  // `SCRUB TABLE ... WITH OPTIONS PHYSICAL` will find any rows where a
   554  // value is NULL for a column that is not-nullable and the only column
   555  // in a family. To test this, a row is created that we later overwrite
   556  // the value for. The value that is inserted is the sentinel value as
   557  // the column is the only one in the family.
   558  func TestScrubPhysicalNonnullableNullInSingleColumnFamily(t *testing.T) {
   559  	defer leaktest.AfterTest(t)()
   560  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   561  	defer s.Stopper().Stop(context.Background())
   562  
   563  	// Create the table and the row entry.
   564  	if _, err := db.Exec(`
   565  CREATE DATABASE t;
   566  CREATE TABLE t.test (k INT PRIMARY KEY, v INT NOT NULL);
   567  INSERT INTO t.test VALUES (217, 314);
   568  `); err != nil {
   569  		t.Fatalf("unexpected error: %s", err)
   570  	}
   571  
   572  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   573  
   574  	// Construct datums for our row values (k, v).
   575  	values := []tree.Datum{tree.NewDInt(217), tree.NewDInt(314)}
   576  
   577  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   578  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   579  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   580  
   581  	// Create the primary index key
   582  	primaryIndexKeyPrefix := sqlbase.MakeIndexKeyPrefix(
   583  		keys.SystemSQLCodec, tableDesc, tableDesc.PrimaryIndex.ID)
   584  	primaryIndexKey, _, err := sqlbase.EncodeIndexKey(
   585  		tableDesc, &tableDesc.PrimaryIndex, colIDtoRowIndex, values, primaryIndexKeyPrefix)
   586  	if err != nil {
   587  		t.Fatalf("unexpected error: %s", err)
   588  	}
   589  
   590  	// Add the family suffix to the key.
   591  	family := tableDesc.Families[0]
   592  	primaryIndexKey = keys.MakeFamilyKey(primaryIndexKey, uint32(family.ID))
   593  
   594  	// Create an empty sentinel value.
   595  	var value roachpb.Value
   596  	value.SetTuple([]byte(nil))
   597  
   598  	if err := kvDB.Put(context.Background(), primaryIndexKey, &value); err != nil {
   599  		t.Fatalf("unexpected error: %s", err)
   600  	}
   601  
   602  	// Run SCRUB and find the errors we created.
   603  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS PHYSICAL`)
   604  	if err != nil {
   605  		t.Fatalf("unexpected error: %s", err)
   606  	}
   607  	defer rows.Close()
   608  	results, err := sqlutils.GetScrubResultRows(rows)
   609  	if err != nil {
   610  		t.Fatalf("unexpected error: %s", err)
   611  	} else if len(results) != 1 {
   612  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   613  	}
   614  
   615  	if result := results[0]; result.ErrorType != string(scrub.UnexpectedNullValueError) {
   616  		t.Fatalf("expected %q error, instead got: %s",
   617  			scrub.UnexpectedNullValueError, result.ErrorType)
   618  	} else if result.Database != "t" {
   619  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   620  	} else if result.Table != "test" {
   621  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   622  	} else if result.PrimaryKey != "(217)" {
   623  		t.Fatalf("expected primaryKey %q, got %q", "(217)", result.PrimaryKey)
   624  	} else if result.Repaired {
   625  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   626  	} else if !strings.Contains(result.Details, `"k": "217"`) {
   627  		t.Fatalf("expected error details to contain `%s`, got %s", `"k": "217"`, result.Details)
   628  	} else if !strings.Contains(result.Details, `"v": "<unset>"`) {
   629  		t.Fatalf("expected error details to contain `%s`, got %s", `"v": "<unset>"`, result.Details)
   630  	}
   631  }
   632  
   633  // TestScrubPhysicalNonnullableNullInMulticolumnFamily tests that
   634  // `SCRUB TABLE ... WITH OPTIONS PHYSICAL` will find any rows where a
   635  // value is NULL for a column that is not-nullable and is not the only
   636  // column in a family. To test this, a row is created that we later
   637  // overwrite the value for. The value that is inserted is missing one of
   638  // the columns that belongs in the family.
   639  func TestScrubPhysicalNonnullableNullInMulticolumnFamily(t *testing.T) {
   640  	defer leaktest.AfterTest(t)()
   641  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   642  	defer s.Stopper().Stop(context.Background())
   643  
   644  	// Create the table and the row entry.
   645  	if _, err := db.Exec(`
   646  CREATE DATABASE t;
   647  CREATE TABLE t.test (k INT PRIMARY KEY, v INT NOT NULL, b INT NOT NULL, FAMILY (k), FAMILY (v, b));
   648  INSERT INTO t.test VALUES (217, 314, 1337);
   649  `); err != nil {
   650  		t.Fatalf("unexpected error: %s", err)
   651  	}
   652  
   653  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   654  
   655  	// Construct datums for our row values (k, v, b).
   656  	values := []tree.Datum{tree.NewDInt(217), tree.NewDInt(314), tree.NewDInt(1337)}
   657  
   658  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   659  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   660  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   661  	colIDtoRowIndex[tableDesc.Columns[2].ID] = 2
   662  
   663  	// Create the primary index key
   664  	primaryIndexKeyPrefix := sqlbase.MakeIndexKeyPrefix(
   665  		keys.SystemSQLCodec, tableDesc, tableDesc.PrimaryIndex.ID)
   666  	primaryIndexKey, _, err := sqlbase.EncodeIndexKey(
   667  		tableDesc, &tableDesc.PrimaryIndex, colIDtoRowIndex, values, primaryIndexKeyPrefix)
   668  	if err != nil {
   669  		t.Fatalf("unexpected error: %s", err)
   670  	}
   671  
   672  	// Add the family suffix to the key, in particular we care about the
   673  	// second column family.
   674  	family := tableDesc.Families[1]
   675  	primaryIndexKey = keys.MakeFamilyKey(primaryIndexKey, uint32(family.ID))
   676  
   677  	// Encode the second column value.
   678  	valueBuf, err := sqlbase.EncodeTableValue(
   679  		[]byte(nil), tableDesc.Columns[1].ID, values[1], []byte(nil))
   680  	if err != nil {
   681  		t.Fatalf("unexpected error: %s", err)
   682  	}
   683  
   684  	// Construct the tuple for the family that is missing a column value, i.e. it is NULL.
   685  	var value roachpb.Value
   686  	value.SetTuple(valueBuf)
   687  
   688  	// Overwrite the existing value.
   689  	if err := kvDB.Put(context.Background(), primaryIndexKey, &value); err != nil {
   690  		t.Fatalf("unexpected error: %s", err)
   691  	}
   692  
   693  	// Run SCRUB and find the errors we created.
   694  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS PHYSICAL`)
   695  	if err != nil {
   696  		t.Fatalf("unexpected error: %s", err)
   697  	}
   698  	defer rows.Close()
   699  	results, err := sqlutils.GetScrubResultRows(rows)
   700  	if err != nil {
   701  		t.Fatalf("unexpected error: %s", err)
   702  	} else if len(results) != 1 {
   703  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   704  	}
   705  
   706  	if result := results[0]; result.ErrorType != string(scrub.UnexpectedNullValueError) {
   707  		t.Fatalf("expected %q error, instead got: %s",
   708  			scrub.UnexpectedNullValueError, result.ErrorType)
   709  	} else if result.Database != "t" {
   710  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   711  	} else if result.Table != "test" {
   712  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   713  	} else if result.PrimaryKey != "(217)" {
   714  		t.Fatalf("expected primaryKey %q, got %q", "(217)", result.PrimaryKey)
   715  	} else if result.Repaired {
   716  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   717  	} else if !strings.Contains(result.Details, `"k": "217"`) {
   718  		t.Fatalf("expected error details to contain `%s`, got %s", `"k": "217"`, result.Details)
   719  	} else if !strings.Contains(result.Details, `"v": "314"`) {
   720  		t.Fatalf("expected error details to contain `%s`, got %s", `"v": "314"`, result.Details)
   721  	} else if !strings.Contains(result.Details, `"b": "<unset>"`) {
   722  		t.Fatalf("expected error details to contain `%s`, got %s", `"b": "<unset>"`, result.Details)
   723  	}
   724  }
   725  
   726  // TestScrubPhysicalUnexpectedFamilyID tests that `SCRUB TABLE ... WITH
   727  // OPTIONS PHYSICAL` will find any rows where a primary index as key
   728  // with an invalid family ID. To test this, a table is made with 2
   729  // families and then the first family is dropped. A row is then inserted
   730  // using the KV client which has the ID of the first family.
   731  func TestScrubPhysicalUnexpectedFamilyID(t *testing.T) {
   732  	defer leaktest.AfterTest(t)()
   733  	t.Skip("currently KV pairs with unexpected family IDs are not noticed by the fetcher")
   734  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   735  	defer s.Stopper().Stop(context.Background())
   736  
   737  	// Create the table and the row entry.
   738  	if _, err := db.Exec(`
   739  CREATE DATABASE t;
   740  CREATE TABLE t.test (
   741  	k INT PRIMARY KEY,
   742  	v1 INT NOT NULL,
   743  	v2 INT NOT NULL,
   744  	FAMILY first (v1),
   745  	FAMILY second (v2)
   746  );
   747  `); err != nil {
   748  		t.Fatalf("unexpected error: %s", err)
   749  	}
   750  
   751  	oldTableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   752  
   753  	// Drop the first column family.
   754  	if _, err := db.Exec(`ALTER TABLE t.test DROP COLUMN v1`); err != nil {
   755  		t.Fatalf("unexpected error: %s", err)
   756  	}
   757  
   758  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   759  
   760  	// Construct datums for our row values (k, v1).
   761  	values := []tree.Datum{tree.NewDInt(217), tree.NewDInt(314)}
   762  
   763  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   764  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   765  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   766  
   767  	// Create the primary index key
   768  	primaryIndexKeyPrefix := sqlbase.MakeIndexKeyPrefix(
   769  		keys.SystemSQLCodec, tableDesc, tableDesc.PrimaryIndex.ID)
   770  	primaryIndexKey, _, err := sqlbase.EncodeIndexKey(
   771  		tableDesc, &tableDesc.PrimaryIndex, colIDtoRowIndex, values, primaryIndexKeyPrefix)
   772  	if err != nil {
   773  		t.Fatalf("unexpected error: %s", err)
   774  	}
   775  
   776  	// Add the correct family suffix to the key.
   777  	primaryIndexKeyWithFamily := keys.MakeFamilyKey(primaryIndexKey, uint32(tableDesc.Families[1].ID))
   778  
   779  	// Encode the second column value.
   780  	valueBuf, err := sqlbase.EncodeTableValue(
   781  		[]byte(nil), tableDesc.Columns[1].ID, values[1], []byte(nil))
   782  	if err != nil {
   783  		t.Fatalf("unexpected error: %s", err)
   784  	}
   785  	var value roachpb.Value
   786  	value.SetTuple(valueBuf)
   787  
   788  	// Insert the value.
   789  	if err := kvDB.Put(context.Background(), primaryIndexKeyWithFamily, &value); err != nil {
   790  		t.Fatalf("unexpected error: %s", err)
   791  	}
   792  
   793  	// Create a k/v with an incorrect family suffix to the key.
   794  	primaryIndexKeyWithFamily = keys.MakeFamilyKey(primaryIndexKey,
   795  		uint32(oldTableDesc.Families[1].ID))
   796  
   797  	// Encode the second column value.
   798  	valueBuf, err = sqlbase.EncodeTableValue(
   799  		[]byte(nil), tableDesc.Columns[1].ID, values[1], []byte(nil))
   800  	if err != nil {
   801  		t.Fatalf("unexpected error: %s", err)
   802  	}
   803  	value = roachpb.Value{}
   804  	value.SetTuple(valueBuf)
   805  
   806  	// Insert the incorrect family k/v.
   807  	if err := kvDB.Put(context.Background(), primaryIndexKeyWithFamily, &value); err != nil {
   808  		t.Fatalf("unexpected error: %s", err)
   809  	}
   810  
   811  	// Run SCRUB and find the errors we created.
   812  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS PHYSICAL`)
   813  	if err != nil {
   814  		t.Fatalf("unexpected error: %s", err)
   815  	}
   816  	defer rows.Close()
   817  	results, err := sqlutils.GetScrubResultRows(rows)
   818  	if err != nil {
   819  		t.Fatalf("unexpected error: %s", err)
   820  	} else if len(results) != 1 {
   821  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   822  	}
   823  
   824  	if result := results[0]; result.ErrorType != string(scrub.UnexpectedNullValueError) {
   825  		t.Fatalf("expected %q error, instead got: %s",
   826  			scrub.UnexpectedNullValueError, result.ErrorType)
   827  	} else if result.Database != "t" {
   828  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   829  	} else if result.Table != "test" {
   830  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   831  	} else if result.PrimaryKey != "(217)" {
   832  		t.Fatalf("expected primaryKey %q, got %q", "(217)", result.PrimaryKey)
   833  	} else if result.Repaired {
   834  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   835  	} else if !strings.Contains(result.Details, `"k": "217"`) {
   836  		t.Fatalf("expected error details to contain `%s`, got %s", `"k": "217"`, result.Details)
   837  	} else if !strings.Contains(result.Details, `"v": "314"`) {
   838  		t.Fatalf("expected error details to contain `%s`, got %s", `"v": "314"`, result.Details)
   839  	} else if !strings.Contains(result.Details, `"b": "<unset>"`) {
   840  		t.Fatalf("expected error details to contain `%s`, got %s", `"b": "<unset>"`, result.Details)
   841  	}
   842  }
   843  
   844  // TestScrubPhysicalIncorrectPrimaryIndexValueColumn tests that
   845  // `SCRUB TABLE ... WITH OPTIONS PHYSICAL` will find any rows where a
   846  // value has an encoded column ID that does not correspond to the table
   847  // descriptor. To test this, a row is inserted using the KV client.
   848  func TestScrubPhysicalIncorrectPrimaryIndexValueColumn(t *testing.T) {
   849  	defer leaktest.AfterTest(t)()
   850  	t.Skip("the test is not failing, as it would be expected")
   851  	s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
   852  	defer s.Stopper().Stop(context.Background())
   853  
   854  	// Create the table and the row entry.
   855  	if _, err := db.Exec(`
   856  CREATE DATABASE t;
   857  CREATE TABLE t.test (k INT PRIMARY KEY, v1 INT, v2 INT);
   858  `); err != nil {
   859  		t.Fatalf("unexpected error: %s", err)
   860  	}
   861  	tableDesc := sqlbase.GetTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
   862  
   863  	// Construct datums for our row values (k, v1, v2).
   864  	values := []tree.Datum{tree.NewDInt(217), tree.NewDInt(314), tree.NewDInt(1337)}
   865  
   866  	colIDtoRowIndex := make(map[sqlbase.ColumnID]int)
   867  	colIDtoRowIndex[tableDesc.Columns[0].ID] = 0
   868  	colIDtoRowIndex[tableDesc.Columns[1].ID] = 1
   869  	colIDtoRowIndex[tableDesc.Columns[2].ID] = 2
   870  
   871  	// Create the primary index key
   872  	primaryIndexKeyPrefix := sqlbase.MakeIndexKeyPrefix(
   873  		keys.SystemSQLCodec, tableDesc, tableDesc.PrimaryIndex.ID)
   874  	primaryIndexKey, _, err := sqlbase.EncodeIndexKey(
   875  		tableDesc, &tableDesc.PrimaryIndex, colIDtoRowIndex, values, primaryIndexKeyPrefix)
   876  	if err != nil {
   877  		t.Fatalf("unexpected error: %s", err)
   878  	}
   879  	// Add the default family suffix to the key.
   880  	primaryIndexKey = keys.MakeFamilyKey(primaryIndexKey, uint32(tableDesc.Families[0].ID))
   881  
   882  	// Encode the second column values. The second column is encoded with
   883  	// a garbage colIDDiff.
   884  	valueBuf, err := sqlbase.EncodeTableValue(
   885  		[]byte(nil), tableDesc.Columns[1].ID, values[1], []byte(nil))
   886  	if err != nil {
   887  		t.Fatalf("unexpected error: %s", err)
   888  	}
   889  
   890  	valueBuf, err = sqlbase.EncodeTableValue(valueBuf, 1000, values[2], []byte(nil))
   891  	if err != nil {
   892  		t.Fatalf("unexpected error: %s", err)
   893  	}
   894  
   895  	// Construct the tuple for the family that is missing a column value, i.e. it is NULL.
   896  	var value roachpb.Value
   897  	value.SetTuple(valueBuf)
   898  
   899  	// Overwrite the existing value.
   900  	if err := kvDB.Put(context.Background(), primaryIndexKey, &value); err != nil {
   901  		t.Fatalf("unexpected error: %s", err)
   902  	}
   903  
   904  	// Run SCRUB and find the errors we created.
   905  	rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS PHYSICAL`)
   906  	if err != nil {
   907  		t.Fatalf("unexpected error: %s", err)
   908  	}
   909  	defer rows.Close()
   910  
   911  	results, err := sqlutils.GetScrubResultRows(rows)
   912  	if err != nil {
   913  		t.Fatalf("unexpected error: %s", err)
   914  	} else if len(results) != 1 {
   915  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   916  	}
   917  
   918  	if result := results[0]; result.ErrorType != string(scrub.UnexpectedNullValueError) {
   919  		t.Fatalf("expected %q error, instead got: %s",
   920  			scrub.UnexpectedNullValueError, result.ErrorType)
   921  	} else if result.Database != "t" {
   922  		t.Fatalf("expected database %q, got %q", "t", result.Database)
   923  	} else if result.Table != "test" {
   924  		t.Fatalf("expected table %q, got %q", "test", result.Table)
   925  	} else if result.PrimaryKey != "(217)" {
   926  		t.Fatalf("expected primaryKey %q, got %q", "(217)", result.PrimaryKey)
   927  	} else if result.Repaired {
   928  		t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
   929  	} else if !strings.Contains(result.Details, `"k": "217"`) {
   930  		t.Fatalf("expected error details to contain `%s`, got %s", `"k": "217"`, result.Details)
   931  	} else if !strings.Contains(result.Details, `"v": "314"`) {
   932  		t.Fatalf("expected error details to contain `%s`, got %s", `"v": "314"`, result.Details)
   933  	} else if !strings.Contains(result.Details, `"b": "<unset>"`) {
   934  		t.Fatalf("expected error details to contain `%s`, got %s", `"b": "<unset>"`, result.Details)
   935  	}
   936  }
   937  
   938  type expectedScrubResult struct {
   939  	ErrorType    string
   940  	Database     string
   941  	Table        string
   942  	PrimaryKey   string
   943  	Repaired     bool
   944  	DetailsRegex string
   945  }
   946  
   947  func checkScrubResult(t *testing.T, res sqlutils.ScrubResult, exp expectedScrubResult) {
   948  	t.Helper()
   949  
   950  	if res.ErrorType != exp.ErrorType {
   951  		t.Errorf("expected %q error, instead got: %s", exp.ErrorType, res.ErrorType)
   952  	}
   953  
   954  	if res.Database != exp.Database {
   955  		t.Errorf("expected database %q, got %q", exp.Database, res.Database)
   956  	}
   957  
   958  	if res.Table != exp.Table {
   959  		t.Errorf("expected table %q, got %q", exp.Table, res.Table)
   960  	}
   961  
   962  	if res.PrimaryKey != exp.PrimaryKey {
   963  		t.Errorf("expected primary key %q, got %q", exp.PrimaryKey, res.PrimaryKey)
   964  	}
   965  	if res.Repaired != exp.Repaired {
   966  		t.Fatalf("expected repaired %v, got %v", exp.Repaired, res.Repaired)
   967  	}
   968  
   969  	if matched, err := regexp.MatchString(exp.DetailsRegex, res.Details); err != nil {
   970  		t.Fatal(err)
   971  	} else if !matched {
   972  		t.Errorf("expected error details to contain `%s`, got `%s`", exp.DetailsRegex, res.Details)
   973  	}
   974  }
   975  
   976  // runScrub runs a SCRUB statement and checks that it returns exactly one scrub
   977  // result and that it matches the expected result.
   978  func runScrub(t *testing.T, db *gosql.DB, scrubStmt string, exp expectedScrubResult) {
   979  	t.Helper()
   980  
   981  	// Run SCRUB and find the FOREIGN KEY violation created.
   982  	rows, err := db.Query(scrubStmt)
   983  	if err != nil {
   984  		t.Fatalf("unexpected error: %s", err)
   985  	}
   986  	defer rows.Close()
   987  
   988  	results, err := sqlutils.GetScrubResultRows(rows)
   989  	if err != nil {
   990  		t.Fatalf("unexpected error: %s", err)
   991  	}
   992  
   993  	if len(results) != 1 {
   994  		t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
   995  	}
   996  	checkScrubResult(t, results[0], exp)
   997  }