Skip to content

Commit 4183e88

Browse files
craig[bot]spilchenarulajmani
committed
155844: sql/inspect: detect dangling secondary index entries in empty spans r=spilchen a=spilchen INSPECT previously skipped index consistency checks when the primary index span had no rows, preventing detection of dangling secondary index entries in empty tables. This change ensures INSPECT always scans secondary indexes, even if the primary is empty, by using an empty predicate to trigger a full scan. Any entries found are considered dangling. Adds a test (TestDanglingIndexEntryInEmptyTable) migrated from pkg/sql/scrub_test.go. Informs #155485 Epic: CRDB-55075 Release note (bug fix): INSPECT now detects dangling secondary index entries even when the primary index spans contain no data. 156009: kv: relax bounds for YCSB splits tests r=arulajmani a=arulajmani We've seen variants of YCSB A and B fail with both higher and lower number of splits. In response, we've changed the bounds of both these variants independently. The commentary suggests the number of splits here should be the same for both variants; this patch makes it such, picking the max upper bound and min lower bound across the two variants. Closes #155951 Release note: None Co-authored-by: Matt Spilchen <matt.spilchen@cockroachlabs.com> Co-authored-by: Arul Ajmani <arulajmani@gmail.com>
3 parents e3c0e97 + 11bdb6f + 584cf20 commit 4183e88

File tree

6 files changed

+110
-116
lines changed

6 files changed

+110
-116
lines changed

pkg/cmd/roachtest/tests/split.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,9 @@ func registerLoadSplits(r registry.Registry) {
335335
maxSize: 10 << 30, // 10 GB
336336
cpuThreshold: 100 * time.Millisecond, // 1/10th of a CPU per second.
337337
// YCSB/A has a zipfian distribution with 50% inserts and 50% updates.
338-
// The number of splits should be between 15-38 after 10 minutes with
338+
// The number of splits should be between 13-38 after 10 minutes with
339339
// 100ms threshold on 8vCPU machines.
340-
minimumRanges: 17,
340+
minimumRanges: 15,
341341
maximumRanges: 40,
342342
initialRangeCount: 2,
343343
load: ycsbSplitLoad{
@@ -362,7 +362,7 @@ func registerLoadSplits(r registry.Registry) {
362362
// The number of splits should be similar to YCSB/A.
363363
cpuThreshold: 100 * time.Millisecond, // 1/10th of a CPU per second.
364364
minimumRanges: 15,
365-
maximumRanges: 35,
365+
maximumRanges: 40,
366366
initialRangeCount: 2,
367367
load: ycsbSplitLoad{
368368
workload: "b",

pkg/sql/inspect/index_consistency_check.go

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -174,41 +174,46 @@ func (c *indexConsistencyCheck) Start(
174174
return errors.Wrap(err, "converting span to query bounds")
175175
}
176176

177-
// Nothing to do if no rows exist in the span.
177+
// If no rows exist in the primary index span, we still need to check for dangling
178+
// secondary index entries. We run the check with an empty predicate, which will
179+
// scan the entire secondary index within the span. Any secondary index entries found
180+
// will be dangling since there are no corresponding primary index rows.
178181
if !hasRows {
179-
return nil
180-
}
181-
182-
if len(bounds.Start) == 0 || len(bounds.End) == 0 {
183-
return errors.AssertionFailedf("query bounds from span didn't produce start or end: %+v", bounds)
184-
}
182+
// Use empty predicate and no query arguments
183+
predicate = ""
184+
queryArgs = []interface{}{}
185+
} else {
186+
if len(bounds.Start) == 0 || len(bounds.End) == 0 {
187+
return errors.AssertionFailedf("query bounds from span didn't produce start or end: %+v", bounds)
188+
}
185189

186-
// Generate SQL predicate from the bounds
187-
pkColNames := colNames(pkColumns)
188-
// Encode column names for SQL usage
189-
encodedPkColNames := make([]string, len(pkColNames))
190-
for i, colName := range pkColNames {
191-
encodedPkColNames[i] = encodeColumnName(colName)
192-
}
193-
predicate, err = spanutils.RenderQueryBounds(
194-
encodedPkColNames, pkColDirs, pkColTypes,
195-
len(bounds.Start), len(bounds.End), true, 1,
196-
)
197-
if err != nil {
198-
return errors.Wrap(err, "rendering query bounds")
199-
}
190+
// Generate SQL predicate from the bounds
191+
pkColNames := colNames(pkColumns)
192+
// Encode column names for SQL usage
193+
encodedPkColNames := make([]string, len(pkColNames))
194+
for i, colName := range pkColNames {
195+
encodedPkColNames[i] = encodeColumnName(colName)
196+
}
197+
predicate, err = spanutils.RenderQueryBounds(
198+
encodedPkColNames, pkColDirs, pkColTypes,
199+
len(bounds.Start), len(bounds.End), true, 1,
200+
)
201+
if err != nil {
202+
return errors.Wrap(err, "rendering query bounds")
203+
}
200204

201-
if strings.TrimSpace(predicate) == "" {
202-
return errors.AssertionFailedf("query bounds from span didn't produce predicate: %+v", bounds)
203-
}
205+
if strings.TrimSpace(predicate) == "" {
206+
return errors.AssertionFailedf("query bounds from span didn't produce predicate: %+v", bounds)
207+
}
204208

205-
// Prepare query arguments: end bounds first, then start bounds
206-
queryArgs = make([]interface{}, 0, len(bounds.End)+len(bounds.Start))
207-
for _, datum := range bounds.End {
208-
queryArgs = append(queryArgs, datum)
209-
}
210-
for _, datum := range bounds.Start {
211-
queryArgs = append(queryArgs, datum)
209+
// Prepare query arguments: end bounds first, then start bounds
210+
queryArgs = make([]interface{}, 0, len(bounds.End)+len(bounds.Start))
211+
for _, datum := range bounds.End {
212+
queryArgs = append(queryArgs, datum)
213+
}
214+
for _, datum := range bounds.Start {
215+
queryArgs = append(queryArgs, datum)
216+
}
212217
}
213218

214219
checkQuery := c.createIndexCheckQuery(

pkg/sql/inspect/index_consistency_check_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,69 @@ func TestDetectIndexConsistencyErrors(t *testing.T) {
560560
}
561561
}
562562

563+
// TestDanglingIndexEntryInEmptyTable verifies that INSPECT can detect dangling
564+
// secondary index entries even when the primary index is completely empty.
565+
func TestDanglingIndexEntryInEmptyTable(t *testing.T) {
566+
defer leaktest.AfterTest(t)()
567+
defer log.Scope(t).Close(t)
568+
ctx := context.Background()
569+
s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
570+
defer s.Stopper().Stop(ctx)
571+
codec := s.ApplicationLayer().Codec()
572+
573+
// Create an empty table with a secondary index.
574+
_, err := db.Exec(`
575+
CREATE DATABASE t;
576+
CREATE TABLE t.test (k INT PRIMARY KEY, v INT);
577+
CREATE INDEX secondary ON t.test (v);
578+
`)
579+
require.NoError(t, err)
580+
581+
tableDesc := desctestutils.TestingGetPublicTableDescriptor(kvDB, codec, "t", "test")
582+
secondaryIndex := tableDesc.PublicNonPrimaryIndexes()[0]
583+
584+
// Manually insert a dangling secondary index entry for a non-existent primary key.
585+
// This creates corruption: a secondary index entry pointing to primary key (10)
586+
// which doesn't exist in the primary index.
587+
values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(314)}
588+
err = insertSecondaryIndexEntry(ctx, codec, values, kvDB, tableDesc, secondaryIndex)
589+
require.NoError(t, err)
590+
591+
// Enable INSPECT command.
592+
_, err = db.Exec(`SET enable_inspect_command = true`)
593+
require.NoError(t, err)
594+
595+
// Run INSPECT and expect it to find the dangling index entry.
596+
// INSPECT should return an error when inconsistencies are found.
597+
_, err = db.Query(`INSPECT TABLE t.test AS OF SYSTEM TIME '-1us' WITH OPTIONS INDEX ALL`)
598+
require.Error(t, err)
599+
require.Contains(t, err.Error(), "INSPECT found inconsistencies")
600+
601+
// Verify the error details using SHOW INSPECT ERRORS.
602+
rows, err := db.Query(`SHOW INSPECT ERRORS FOR TABLE t.test WITH DETAILS`)
603+
require.NoError(t, err)
604+
defer rows.Close()
605+
606+
var errorType, dbName, schemaName, tableName, primaryKey, aost, details string
607+
var jobID int64
608+
found := false
609+
for rows.Next() {
610+
err = rows.Scan(&errorType, &dbName, &schemaName, &tableName, &primaryKey, &jobID, &aost, &details)
611+
require.NoError(t, err)
612+
found = true
613+
614+
// Verify the error is a dangling secondary index entry.
615+
require.Equal(t, "dangling_secondary_index_entry", errorType)
616+
require.Equal(t, "t", dbName)
617+
require.Equal(t, "test", tableName)
618+
require.Equal(t, "'(10)'", primaryKey)
619+
// Verify details contain the expected column value.
620+
require.Contains(t, details, `"v": "314"`)
621+
}
622+
623+
require.True(t, found, "expected to find at least one inspect error, but found none")
624+
}
625+
563626
func TestIndexConsistencyWithReservedWordColumns(t *testing.T) {
564627
defer leaktest.AfterTest(t)()
565628
defer log.Scope(t).Close(t)

pkg/sql/inspect_job.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ func InspectChecksForTable(
132132
) ([]*jobspb.InspectDetails_Check, error) {
133133
checks := []*jobspb.InspectDetails_Check{}
134134

135+
// Skip virtual tables since they don't have physical storage to inspect.
136+
if table.IsVirtualTable() {
137+
return checks, nil
138+
}
139+
135140
for _, index := range table.PublicNonPrimaryIndexes() {
136141
if isUnsupportedIndexForIndexConsistencyCheck(index, table) {
137142
if p != nil {

pkg/sql/logictest/testdata/logic_test/inspect

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ SELECT * FROM last_inspect_job;
138138
----
139139
succeeded 1
140140

141+
# TODO(155483): use a really old timestamp from before 'foo' was created.
141142
statement ok
142-
INSPECT TABLE foo AS OF SYSTEM TIME 1 WITH OPTIONS INDEX (idx_c1,idx_c2);
143+
INSPECT TABLE foo AS OF SYSTEM TIME '-50ms' WITH OPTIONS INDEX (idx_c1,idx_c2);
143144

144145
query TI
145146
SELECT * FROM last_inspect_job;
@@ -163,7 +164,7 @@ INSPECT DATABASE test;
163164
query TI
164165
SELECT * FROM last_inspect_job;
165166
----
166-
succeeded 28
167+
succeeded 2
167168

168169
statement error index "idx_c3" does not exist
169170
INSPECT DATABASE test WITH OPTIONS INDEX (idx_c1, idx_c3);

pkg/sql/scrub_test.go

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -203,86 +203,6 @@ func addIndexEntryForDatums(
203203
return kvDB.Put(context.Background(), entry.Key, &entry.Value)
204204
}
205205

206-
// TestScrubIndexDanglingIndexReference tests that
207-
// `SCRUB TABLE ... INDEX“ will find dangling index references, which
208-
// are index entries that have no corresponding primary k/v. To test
209-
// this an index entry is generated and inserted. This creates a
210-
// dangling index error as the corresponding primary k/v is not equal.
211-
func TestScrubIndexDanglingIndexReference(t *testing.T) {
212-
defer leaktest.AfterTest(t)()
213-
defer log.Scope(t).Close(t)
214-
s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
215-
defer s.Stopper().Stop(context.Background())
216-
217-
// Create the table and the row entry.
218-
if _, err := db.Exec(`
219-
CREATE DATABASE t;
220-
CREATE TABLE t.test (k INT PRIMARY KEY, v INT);
221-
CREATE INDEX secondary ON t.test (v);
222-
`); err != nil {
223-
t.Fatalf("unexpected error: %s", err)
224-
}
225-
226-
tableDesc := desctestutils.TestingGetPublicTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "test")
227-
secondaryIndex := tableDesc.PublicNonPrimaryIndexes()[0]
228-
// Construct datums and secondary k/v for our row values (k, v).
229-
values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(314)}
230-
231-
// Put the new secondary k/v into the database.
232-
if err := addIndexEntryForDatums(values, kvDB, tableDesc, secondaryIndex); err != nil {
233-
t.Fatalf("unexpected error: %s", err)
234-
}
235-
236-
// Run SCRUB and find the index errors we created.
237-
rows, err := db.Query(`EXPERIMENTAL SCRUB TABLE t.test WITH OPTIONS INDEX ALL`)
238-
if err != nil {
239-
t.Fatalf("unexpected error: %s", err)
240-
}
241-
defer rows.Close()
242-
243-
results, err := sqlutils.GetInspectResultRows(rows)
244-
if err != nil {
245-
t.Fatalf("unexpected error: %s", err)
246-
}
247-
248-
if len(results) != 1 {
249-
t.Fatalf("expected 1 result, got %d. got %#v", len(results), results)
250-
}
251-
if result := results[0]; result.ErrorType != scrub.DanglingIndexReferenceError {
252-
t.Fatalf("expected %q error, instead got: %s",
253-
scrub.DanglingIndexReferenceError, result.ErrorType)
254-
} else if result.Database != "t" {
255-
t.Fatalf("expected database %q, got %q", "t", result.Database)
256-
} else if result.Table != "test" {
257-
t.Fatalf("expected table %q, got %q", "test", result.Table)
258-
} else if result.PrimaryKey != "(10)" {
259-
t.Fatalf("expected primaryKey %q, got %q", "(10)", result.PrimaryKey)
260-
} else if result.Repaired {
261-
t.Fatalf("expected repaired %v, got %v", false, result.Repaired)
262-
} else if !strings.Contains(result.Details, `"v": "314"`) {
263-
t.Fatalf("expected error details to contain `%s`, got %s", `"v": "314"`, result.Details)
264-
}
265-
266-
// Run SCRUB DATABASE to make sure it also catches the problem.
267-
rows, err = db.Query(`EXPERIMENTAL SCRUB DATABASE t`)
268-
if err != nil {
269-
t.Fatalf("unexpected error: %+v", err)
270-
}
271-
defer rows.Close()
272-
scrubDatabaseResults, err := sqlutils.GetInspectResultRows(rows)
273-
if err != nil {
274-
t.Fatalf("unexpected error: %s", err)
275-
} else if len(scrubDatabaseResults) != 1 {
276-
t.Fatalf("expected 1 result, got %d. got %#v", len(scrubDatabaseResults), scrubDatabaseResults)
277-
} else if !(scrubDatabaseResults[0].ErrorType == results[0].ErrorType &&
278-
scrubDatabaseResults[0].Database == results[0].Database &&
279-
scrubDatabaseResults[0].Table == results[0].Table &&
280-
scrubDatabaseResults[0].Details == results[0].Details) {
281-
t.Fatalf("expected results to be equal, SCRUB TABLE got %v. SCRUB DATABASE got %v",
282-
results, scrubDatabaseResults)
283-
}
284-
}
285-
286206
// TestScrubIndexCatchesStoringMismatch tests that
287207
// `SCRUB TABLE ... INDEX ALL` will fail if an index entry only differs
288208
// by its STORING values. To test this, a row's underlying secondary

0 commit comments

Comments
 (0)