Skip to content

Commit e1c7374

Browse files
craig[bot]spilchen
andcommitted
Merge #155837
155837: sql/inspect: fix AS OF SYSTEM TIME span bounds in index checks r=spilchen a=spilchen INSPECT's index consistency checks could miss spans when using AS OF SYSTEM TIME if those spans had no data at the current time but did at the historical timestamp. This was due to SpanToQueryBounds() using the current time for KV scans instead of the historical time. This commit: - Adds an asOf parameter to SpanToQueryBounds() for historical scans - Updates INSPECT to pass the query's timestamp - Keeps existing behavior for TTL and tests by passing an empty timestamp The issue was uncovered while converting TestScrubIndexMissingIndexEntry to INSPECT. The test is now TestMissingIndexEntryWithHistoricalQuery and validates correctness of historical scans. Informs: #155485 Epic: CRDB-55075 Release note (bug fix): INSPECT now correctly checks index consistency at the historical timestamp when using AS OF SYSTEM TIME, even for spans with no current data. Co-authored-by: Matt Spilchen <matt.spilchen@cockroachlabs.com>
2 parents 8cca32d + aa840e9 commit e1c7374

File tree

8 files changed

+107
-62
lines changed

8 files changed

+107
-62
lines changed

pkg/sql/inspect/index_consistency_check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (c *indexConsistencyCheck) Start(
168168
alloc := &tree.DatumAlloc{}
169169
bounds, hasRows, err := spanutils.SpanToQueryBounds(
170170
ctx, cfg.DB.KV(), cfg.Codec, pkColIDs, pkColTypes, pkColDirs,
171-
len(c.tableDesc.GetFamilies()), span, alloc,
171+
len(c.tableDesc.GetFamilies()), span, alloc, c.asOf,
172172
)
173173
if err != nil {
174174
return errors.Wrap(err, "converting span to query bounds")

pkg/sql/inspect/index_consistency_check_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,3 +614,70 @@ func TestIndexConsistencyWithReservedWordColumns(t *testing.T) {
614614
require.InEpsilon(t, 1.0, fractionCompleted, 0.01, "progress should reach 100%% on successful completion")
615615
requireCheckCountsMatch(t, r, jobID)
616616
}
617+
618+
// TestMissingIndexEntryWithHistoricalQuery verifies that INSPECT can detect
619+
// missing index entries when querying at a historical timestamp (AS OF SYSTEM
620+
// TIME), including cases where data has since been deleted.
621+
//
622+
// This test overlaps with TestDetectIndexConsistencyErrors but is kept to
623+
// expand coverage. It exercises scenarios that TestDetectIndexConsistencyErrors
624+
// missed and specifically validates INSPECT’s handling of AS OF SYSTEM TIME and
625+
// mixed-case table names.
626+
func TestMissingIndexEntryWithHistoricalQuery(t *testing.T) {
627+
defer leaktest.AfterTest(t)()
628+
defer log.Scope(t).Close(t)
629+
ctx := context.Background()
630+
s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
631+
defer s.Stopper().Stop(ctx)
632+
r := sqlutils.MakeSQLRunner(db)
633+
codec := s.ApplicationLayer().Codec()
634+
635+
// Create the table and the row entry.
636+
// We use a table with mixed case as a regression case for #38184.
637+
r.ExecMultiple(t,
638+
`CREATE DATABASE t`,
639+
`CREATE TABLE t."tEst" ("K" INT PRIMARY KEY, v INT)`,
640+
`CREATE INDEX secondary ON t."tEst" (v)`,
641+
`INSERT INTO t."tEst" VALUES (10, 20)`,
642+
)
643+
644+
// Construct datums for our row values (k, v).
645+
values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(20)}
646+
tableDesc := desctestutils.TestingGetPublicTableDescriptor(kvDB, codec, "t", "tEst")
647+
secondaryIndex := tableDesc.PublicNonPrimaryIndexes()[0]
648+
649+
// Delete the secondary index entry to create corruption.
650+
err := deleteSecondaryIndexEntry(ctx, codec, values, kvDB, tableDesc, secondaryIndex)
651+
require.NoError(t, err)
652+
653+
// Enable INSPECT command.
654+
r.Exec(t, `SET enable_inspect_command = true`)
655+
656+
// Run INSPECT and expect it to find the missing index entry.
657+
// INSPECT returns an error when inconsistencies are found.
658+
_, err = db.Query(`INSPECT TABLE t."tEst" WITH OPTIONS INDEX ALL`)
659+
require.Error(t, err, "expected INSPECT to return an error when inconsistencies are found")
660+
require.Contains(t, err.Error(), "INSPECT found inconsistencies")
661+
662+
// Run again with AS OF SYSTEM TIME.
663+
time.Sleep(1 * time.Millisecond)
664+
_, err = db.Query(`INSPECT TABLE t."tEst" AS OF SYSTEM TIME '-1ms' WITH OPTIONS INDEX ALL`)
665+
require.Error(t, err, "expected INSPECT to return an error when inconsistencies are found")
666+
require.Contains(t, err.Error(), "INSPECT found inconsistencies")
667+
668+
// Verify that AS OF SYSTEM TIME actually operates in the past by:
669+
// 1. Getting a timestamp before we delete the row
670+
// 2. Deleting the entire row
671+
// 3. Running INSPECT at the historical timestamp
672+
// At that historical timestamp, the row existed and was corrupted, so INSPECT
673+
// should still find the corruption even though the row no longer exists.
674+
ts := r.QueryStr(t, `SELECT cluster_logical_timestamp()`)[0][0]
675+
r.Exec(t, `DELETE FROM t."tEst"`)
676+
677+
_, err = db.Query(fmt.Sprintf(
678+
`INSPECT TABLE t."tEst" AS OF SYSTEM TIME '%s' WITH OPTIONS INDEX ALL`, ts,
679+
))
680+
require.Error(t, err, "expected INSPECT to find corruption at historical timestamp")
681+
require.Contains(t, err.Error(), "INSPECT found inconsistencies",
682+
"INSPECT should detect the missing index entry that existed at the historical timestamp")
683+
}

pkg/sql/scrub_test.go

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -30,64 +30,6 @@ import (
3030
"github.com/cockroachdb/errors"
3131
)
3232

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-
defer log.Scope(t).Close(t)
41-
s, db, kvDB := serverutils.StartServer(t, base.TestServerArgs{})
42-
defer s.Stopper().Stop(context.Background())
43-
r := sqlutils.MakeSQLRunner(db)
44-
45-
// Create the table and the row entry.
46-
// We use a table with mixed as a regression case for #38184.
47-
if _, err := db.Exec(`
48-
CREATE DATABASE t;
49-
CREATE TABLE t."tEst" ("K" INT PRIMARY KEY, v INT);
50-
CREATE INDEX secondary ON t."tEst" (v);
51-
INSERT INTO t."tEst" VALUES (10, 20);
52-
`); err != nil {
53-
t.Fatalf("unexpected error: %s", err)
54-
}
55-
56-
// Construct datums for our row values (k, v).
57-
values := []tree.Datum{tree.NewDInt(10), tree.NewDInt(20)}
58-
tableDesc := desctestutils.TestingGetPublicTableDescriptor(kvDB, keys.SystemSQLCodec, "t", "tEst")
59-
secondaryIndex := tableDesc.PublicNonPrimaryIndexes()[0]
60-
if err := removeIndexEntryForDatums(values, kvDB, tableDesc, secondaryIndex); err != nil {
61-
t.Fatalf("unexpected error: %s", err.Error())
62-
}
63-
64-
// Run SCRUB and find the index errors we created.
65-
exp := []scrubtestutils.ExpectedScrubResult{
66-
{
67-
ErrorType: scrub.MissingIndexEntryError,
68-
Database: "t",
69-
Table: "tEst",
70-
PrimaryKey: "(10)",
71-
Repaired: false,
72-
DetailsRegex: `"v": "20"`,
73-
},
74-
}
75-
scrubtestutils.RunScrub(t, db, `EXPERIMENTAL SCRUB TABLE t."tEst" WITH OPTIONS INDEX ALL`, exp)
76-
// Run again with AS OF SYSTEM TIME.
77-
time.Sleep(1 * time.Millisecond)
78-
scrubtestutils.RunScrub(t, db, `EXPERIMENTAL SCRUB TABLE t."tEst" AS OF SYSTEM TIME '-1ms' WITH OPTIONS INDEX ALL`, exp)
79-
80-
// Verify that AS OF SYSTEM TIME actually operates in the past.
81-
ts := r.QueryStr(t, `SELECT cluster_logical_timestamp()`)[0][0]
82-
r.Exec(t, `DELETE FROM t."tEst"`)
83-
scrubtestutils.RunScrub(
84-
t, db, fmt.Sprintf(
85-
`EXPERIMENTAL SCRUB TABLE t."tEst" AS OF SYSTEM TIME '%s' WITH OPTIONS INDEX ALL`, ts,
86-
),
87-
exp,
88-
)
89-
}
90-
9133
// TestScrubIndexPartialIndex tests that SCRUB catches various anomalies in the data contained in a
9234
// partial secondary index.
9335
func TestScrubIndexPartialIndex(t *testing.T) {

pkg/sql/spanutils/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ go_library(
1515
"//pkg/sql/rowenc",
1616
"//pkg/sql/sem/tree",
1717
"//pkg/sql/types",
18+
"//pkg/util/hlc",
1819
"@com_github_cockroachdb_errors//:errors",
1920
],
2021
)
@@ -45,6 +46,7 @@ go_test(
4546
"//pkg/testutils/skip",
4647
"//pkg/testutils/sqlutils",
4748
"//pkg/testutils/testcluster",
49+
"//pkg/util/hlc",
4850
"//pkg/util/leaktest",
4951
"//pkg/util/log",
5052
"@com_github_stretchr_testify//require",

pkg/sql/spanutils/query_bounds.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/cockroachdb/cockroach/pkg/sql/rowenc"
2020
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
2121
"github.com/cockroachdb/cockroach/pkg/sql/types"
22+
"github.com/cockroachdb/cockroach/pkg/util/hlc"
2223
"github.com/cockroachdb/errors"
2324
)
2425

@@ -84,18 +85,46 @@ func SpanToQueryBounds(
8485
numFamilies int,
8586
span roachpb.Span,
8687
alloc *tree.DatumAlloc,
88+
asOf hlc.Timestamp,
8789
) (bounds QueryBounds, hasRows bool, _ error) {
8890
partialStartKey := span.Key
8991
partialEndKey := span.EndKey
90-
startKeyValues, err := kvDB.Scan(ctx, partialStartKey, partialEndKey, int64(numFamilies))
92+
93+
var startKeyValues []kv.KeyValue
94+
var err error
95+
if asOf.IsEmpty() {
96+
startKeyValues, err = kvDB.Scan(ctx, partialStartKey, partialEndKey, int64(numFamilies))
97+
} else {
98+
err = kvDB.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
99+
if err := txn.SetFixedTimestamp(ctx, asOf); err != nil {
100+
return err
101+
}
102+
var scanErr error
103+
startKeyValues, scanErr = txn.Scan(ctx, partialStartKey, partialEndKey, int64(numFamilies))
104+
return scanErr
105+
})
106+
}
91107
if err != nil {
92108
return bounds, false, errors.Wrapf(err, "scan error startKey=%x endKey=%x", []byte(partialStartKey), []byte(partialEndKey))
93109
}
94110
// If span has 0 rows then return early - it will not be processed.
95111
if len(startKeyValues) == 0 {
96112
return bounds, false, nil
97113
}
98-
endKeyValues, err := kvDB.ReverseScan(ctx, partialStartKey, partialEndKey, int64(numFamilies))
114+
115+
var endKeyValues []kv.KeyValue
116+
if asOf.IsEmpty() {
117+
endKeyValues, err = kvDB.ReverseScan(ctx, partialStartKey, partialEndKey, int64(numFamilies))
118+
} else {
119+
err = kvDB.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
120+
if err := txn.SetFixedTimestamp(ctx, asOf); err != nil {
121+
return err
122+
}
123+
var scanErr error
124+
endKeyValues, scanErr = txn.ReverseScan(ctx, partialStartKey, partialEndKey, int64(numFamilies))
125+
return scanErr
126+
})
127+
}
99128
if err != nil {
100129
return bounds, false, errors.Wrapf(err, "reverse scan error startKey=%x endKey=%x", []byte(partialStartKey), []byte(partialEndKey))
101130
}

pkg/sql/spanutils/query_bounds_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
2424
"github.com/cockroachdb/cockroach/pkg/testutils/skip"
2525
"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
26+
"github.com/cockroachdb/cockroach/pkg/util/hlc"
2627
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
2728
"github.com/cockroachdb/cockroach/pkg/util/log"
2829
"github.com/stretchr/testify/require"
@@ -202,7 +203,7 @@ func TestSpanToQueryBounds(t *testing.T) {
202203
actualBounds, actualHasRows, err := spanutils.SpanToQueryBounds(ctx, kvDB, codec, pkColIDs, pkColTypes, pkColDirs, 1, roachpb.Span{
203204
Key: startKey,
204205
EndKey: endKey,
205-
}, &alloc)
206+
}, &alloc, hlc.Timestamp{})
206207

207208
// Verify results.
208209
require.NoError(t, err)
@@ -423,6 +424,7 @@ func TestSpanToQueryBoundsCompositeKeys(t *testing.T) {
423424
EndKey: endKey,
424425
},
425426
&alloc,
427+
hlc.Timestamp{},
426428
)
427429

428430
// Verify results.

pkg/sql/ttl/ttljob/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ go_library(
4848
"//pkg/sql/types",
4949
"//pkg/util/admission/admissionpb",
5050
"//pkg/util/ctxgroup",
51+
"//pkg/util/hlc",
5152
"//pkg/util/log",
5253
"//pkg/util/metric",
5354
"//pkg/util/metric/aggmetric",

pkg/sql/ttl/ttljob/ttljob_processor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/cockroachdb/cockroach/pkg/sql/types"
3838
"github.com/cockroachdb/cockroach/pkg/util/admission/admissionpb"
3939
"github.com/cockroachdb/cockroach/pkg/util/ctxgroup"
40+
"github.com/cockroachdb/cockroach/pkg/util/hlc"
4041
"github.com/cockroachdb/cockroach/pkg/util/log"
4142
"github.com/cockroachdb/cockroach/pkg/util/quotapool"
4243
"github.com/cockroachdb/cockroach/pkg/util/retry"
@@ -356,6 +357,7 @@ func (t *ttlProcessor) work(ctx context.Context, output execinfra.RowReceiver) e
356357
numFamilies,
357358
span,
358359
&alloc,
360+
hlc.Timestamp{}, // Use current time bounds; TTL only deletes live rows.
359361
); err != nil {
360362
return errors.Wrapf(err, "SpanToQueryBounds error index=%d span=%s", i, span)
361363
} else if hasRows {

0 commit comments

Comments
 (0)