Skip to content
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
30b1f12
table: Global index did not correctly use partID+_tidb_rowid
mjonss Dec 29, 2025
22d0a74
ddl, meta, tablecodec: add version metadata for global index backward…
mjonss Dec 30, 2025
e102f6a
bazel_prepare
mjonss Jan 1, 2026
fd722bb
Cleanup
mjonss Jan 1, 2026
6059bde
Linting
mjonss Jan 1, 2026
2e01584
Linting
mjonss Jan 1, 2026
f13baf2
Removed debug code that failed linting
mjonss Jan 1, 2026
dd33176
Cleanup
mjonss Jan 1, 2026
3b824df
Added test for old version global index
mjonss Jan 1, 2026
7013ec4
Fixed case where INSERT did not use PartitionHandle for adding index …
mjonss Jan 1, 2026
ee50be1
Updated test and fixed found issue
mjonss Jan 1, 2026
ef6420f
Fixed issue with clustered IntHandle tables
mjonss Jan 1, 2026
cac17ca
temp commit for trouble-shooting how to check if Clustered or not...
mjonss Jan 2, 2026
ea3d427
pkg/ddl: fix GlobalIndexVersion assignment for tables with inline PRI…
mjonss Jan 2, 2026
c175fcb
cleanup of debug in test
mjonss Jan 2, 2026
236ef4d
use patched tikv-server to run realtikv tests
tangenta Jan 5, 2026
78e8421
Update pkg/tablecodec/tablecodec.go
mjonss Jan 7, 2026
d0fb8f3
Addressed review comment about pre-scanning PK for setting Clustered …
mjonss Jan 7, 2026
af46056
Fixed review comment about checking V1 only once per batch of fetchRo…
mjonss Jan 7, 2026
20bd0f5
Linting
mjonss Jan 7, 2026
a3c4848
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 7, 2026
8ed5a61
Addressed review comments, moved logic for using partitioned actualHa…
mjonss Jan 9, 2026
eee5ec1
Linting
mjonss Jan 11, 2026
732ed50
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 11, 2026
7f10e0d
tablecodec: Global Index version V2, pid only in key for non-unique n…
mjonss Jan 14, 2026
91efbc7
Temporary test for TiKV Global Index V2
mjonss Jan 14, 2026
e1a3fa2
Removed use of special built tikv, since V1 is now merged into tikv/m…
mjonss Jan 15, 2026
a18ee7b
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 15, 2026
d3d17e0
Removed non-needed change
mjonss Jan 15, 2026
ea589be
Removed more non-needed changes
mjonss Jan 16, 2026
edc40d8
Added back a comment that should not have been removed...
mjonss Jan 16, 2026
191a0b3
Merge branch 'global-index-non-clustered-non-unique-add-partid-to-key…
mjonss Jan 19, 2026
da153a8
ddl: add test for global index key/value format across V0, V1, V2
mjonss Jan 19, 2026
bfcc751
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 21, 2026
c4c252d
Added tests for copilots review comments
mjonss Jan 21, 2026
b7ad25c
Merge branch 'global-index-non-clustered-non-unique-add-partid-to-key…
mjonss Jan 21, 2026
43e845a
Fixed latent bug and updated test comment
mjonss Jan 21, 2026
1988be7
minor refactoring and removed non-needed code
mjonss Jan 21, 2026
9a7c2b6
Merged new tests into the same test file
mjonss Jan 21, 2026
27c42a2
Apply suggestions from code review
mjonss Jan 23, 2026
0473d15
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 23, 2026
d5c43fa
Fixed issue with nested PartitionHandle
mjonss Jan 23, 2026
fc2ce59
bazel_prepare
mjonss Jan 26, 2026
d7a3a30
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 26, 2026
b1565a9
Fix overwritten global index entries for unique null keys
mjonss Jan 30, 2026
cb95d58
Merge remote-tracking branch 'pingcap/master' into global-index-non-c…
mjonss Jan 30, 2026
34df699
Test fix
mjonss Feb 3, 2026
c1045f7
Removed using subtests in tablecodec_test
mjonss Feb 3, 2026
a030ee7
Merge branch 'global-index-non-clustered-non-unique-add-partid-to-key…
mjonss Feb 3, 2026
b396cd2
Updated test case from V1 to V2
mjonss Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,15 @@ bazel_ci_prepare:

.PHONY: bazel_ci_simple_prepare
bazel_ci_simple_prepare:
@mkdir -p ./bin; \
if [ ! -f ./bin/tikv-server.tar.gz ]; then \
echo "Downloading tikv-server tarball"; \
wget -q -O ./bin/tikv-server.tar.gz "https://fileserver.pingcap.net/download/builds/devbuild/9652/tikv-linux-amd64.tar.gz" || true; \
else \
echo "tikv-server tarball already exists, skipping download"; \
fi; \
echo "Extracting tikv-server"; \
Comment on lines 667 to +675
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes to bazel_ci_simple_prepare target (downloading and extracting tikv-server tarball) appear unrelated to the global index V2 changes described in the PR. These infrastructure changes should either be in a separate PR or explained in the PR description. If this is intentionally included, please explain why it's necessary for the global index V2 functionality.

Suggested change
bazel_ci_simple_prepare:
@mkdir -p ./bin; \
if [ ! -f ./bin/tikv-server.tar.gz ]; then \
echo "Downloading tikv-server tarball"; \
wget -q -O ./bin/tikv-server.tar.gz "https://fileserver.pingcap.net/download/builds/devbuild/9652/tikv-linux-amd64.tar.gz" || true; \
else \
echo "tikv-server tarball already exists, skipping download"; \
fi; \
echo "Extracting tikv-server"; \
# NOTE:
# - This target is used by Bazel-based CI jobs that exercise global index V2.
# - Those tests require a local tikv-server binary to start a TiKV instance during the run.
# - We therefore download and extract a prebuilt tikv-server tarball into ./bin as part of CI prepare.
bazel_ci_simple_prepare:
@mkdir -p ./bin; \
if [ ! -f ./bin/tikv-server.tar.gz ]; then \
echo "Downloading tikv-server tarball required for Bazel CI (global index V2 tests)"; \
wget -q -O ./bin/tikv-server.tar.gz "https://fileserver.pingcap.net/download/builds/devbuild/9652/tikv-linux-amd64.tar.gz" || true; \
else \
echo "tikv-server tarball already exists, skipping download"; \
fi; \
echo "Extracting tikv-server binary for Bazel CI use"; \

Copilot uses AI. Check for mistakes.
tar -xzf ./bin/tikv-server.tar.gz -C ./bin/ || true
bazel $(BAZEL_GLOBAL_CONFIG) run $(BAZEL_CMD_CONFIG) //:gazelle
bazel $(BAZEL_GLOBAL_CONFIG) run $(BAZEL_CMD_CONFIG) \
--run_under="cd $(CURDIR) && " \
Expand Down
59 changes: 53 additions & 6 deletions pkg/ddl/create_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,25 @@ func BuildTableInfo(
existedColsMap[v.Name.L] = struct{}{}
}
foreignKeyID := tbInfo.MaxForeignKeyID

// This pre-scan is necessary because index building in the main loop may need to check
// tbInfo.HasClusteredIndex() before the PRIMARY KEY constraint is fully processed.
for _, constr := range constraints {
if constr.Tp == ast.ConstraintPrimaryKey {
isSingleIntPK := isSingleIntPKFromTableInfo(constr, tbInfo)

if ShouldBuildClusteredIndex(ctx.GetClusteredIndexDefMode(), constr.Option, isSingleIntPK) {
if isSingleIntPK {
tbInfo.PKIsHandle = true
} else {
tbInfo.IsCommonHandle = true
tbInfo.CommonHandleVersion = 1
}
}
break // Only one PRIMARY KEY possible
}
}

for _, constr := range constraints {
var hiddenCols []*model.ColumnInfo
if constr.Tp != ast.ConstraintColumnar {
Expand Down Expand Up @@ -1380,7 +1399,7 @@ func BuildTableInfo(
// all partial primary key.
return nil, dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs("create an primary key with partial index is not supported")
}
isSingleIntPK := isSingleIntPK(constr, lastCol)
isSingleIntPK := isSingleIntPKFromCol(constr, lastCol)
if ShouldBuildClusteredIndex(ctx.GetClusteredIndexDefMode(), constr.Option, isSingleIntPK) {
if isSingleIntPK {
tbInfo.PKIsHandle = true
Expand Down Expand Up @@ -1676,18 +1695,46 @@ func addIndexForForeignKey(ctx *metabuild.Context, tbInfo *model.TableInfo) erro
return nil
}

func isSingleIntPK(constr *ast.Constraint, lastCol *model.ColumnInfo) bool {
if len(constr.Keys) != 1 {
return false
}
switch lastCol.GetType() {
func isIntCol(col *model.ColumnInfo) bool {
switch col.GetType() {
case mysql.TypeLong, mysql.TypeLonglong,
mysql.TypeTiny, mysql.TypeShort, mysql.TypeInt24:
return true
}
return false
}

// isSingleIntPKFromTableInfo determines if a constraint represents a single integer primary key
// by looking up the column information from the table info. This is used during the pre-scan
// phase before CheckPKOnGeneratedColumn is called.
func isSingleIntPKFromTableInfo(constr *ast.Constraint, tbInfo *model.TableInfo) bool {
// Multi-column PKs are not single integer PKs
if len(constr.Keys) != 1 {
return false
}

// Expression-based PKs (e.g., PRIMARY KEY ((col+1))) are not single integer PKs
if constr.Keys[0].Expr != nil {
return false
}

// Find the column in the table
colName := constr.Keys[0].Column.Name.L
for _, col := range tbInfo.Columns {
if col.Name.L == colName {
return isIntCol(col)
}
}
return false
}

func isSingleIntPKFromCol(constr *ast.Constraint, lastCol *model.ColumnInfo) bool {
if len(constr.Keys) != 1 {
return false
}
return isIntCol(lastCol)
}

// ShouldBuildClusteredIndex is used to determine whether the CREATE TABLE statement should build a clustered index table.
func ShouldBuildClusteredIndex(mode vardef.ClusteredIndexDefMode, opt *ast.IndexOption, isSingleIntPK bool) bool {
if opt == nil || opt.PrimaryKeyTp == ast.PrimaryKeyTypeDefault {
Expand Down
25 changes: 24 additions & 1 deletion pkg/ddl/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,20 @@ func BuildIndexInfo(
idxInfo.Tp = indexOption.Tp
}
idxInfo.Global = indexOption.Global
// Set global index version for new global indexes.
// Version 2 is needed for non-clustered tables with non-unique global indexes
// to prevent collisions after EXCHANGE PARTITION due to duplicate _tidb_rowid values.
// V2 encodes partition ID in the key only (not in the value) for storage efficiency.
// Clustered tables and unique indexes don't have this issue and use version 0.
idxInfo.GlobalIndexVersion = 0
if indexOption.Global && !idxInfo.Unique && !tblInfo.HasClusteredIndex() {
idxInfo.GlobalIndexVersion = model.GlobalIndexVersionV2
failpoint.Inject("SetGlobalIndexVersion", func(val failpoint.Value) {
if valInt, ok := val.(int); ok {
idxInfo.GlobalIndexVersion = uint8(valInt)
}
})
}

conditionString, err := CheckAndBuildIndexConditionString(tblInfo, indexOption.Condition)
if err != nil {
Expand Down Expand Up @@ -2454,7 +2468,16 @@ func (w *baseIndexWorker) fetchRowColVals(txn kv.Transaction, taskRange reorgBac
if index.Meta().HasCondition() {
return false, dbterror.ErrUnsupportedAddPartialIndex.GenWithStackByArgs("add partial index without fast reorg")
}
idxRecord, err1 := w.getIndexRecord(index.Meta(), handle, recordKey)
actualHandle := handle
// For global indexes V1+ on partitioned tables, we need to wrap the handle
// with the partition ID to create a PartitionHandle.
// This is critical for non-clustered tables after EXCHANGE PARTITION,
// where duplicate _tidb_rowid values exist across partitions.
// Legacy indexes (version 0) don't use PartitionHandle in the key.
if index.Meta().Global && index.Meta().GlobalIndexVersion >= model.GlobalIndexVersionV1 {
actualHandle = kv.NewPartitionHandle(taskRange.physicalTable.GetPhysicalID(), handle)
}
idxRecord, err1 := w.getIndexRecord(index.Meta(), actualHandle, recordKey)
if err1 != nil {
return false, errors.Trace(err1)
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/ddl/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,8 +624,18 @@ func buildTablePartitionInfo(ctx *metabuild.Context, s *ast.PartitionOptions, tb
return dbterror.ErrWrongNameForIndex.GenWithStackByArgs(idxUpdate.Name)
}
dupCheck[strings.ToLower(idxUpdate.Name)] = struct{}{}
tbInfo.Indices[idxOffset].GlobalIndexVersion = 0
if idxUpdate.Option != nil && idxUpdate.Option.Global {
tbInfo.Indices[idxOffset].Global = true
// Only use V2 for non-clustered tables with non-unique global indexes
if !tbInfo.Indices[idxOffset].Unique && !tbInfo.HasClusteredIndex() {
tbInfo.Indices[idxOffset].GlobalIndexVersion = model.GlobalIndexVersionV2
failpoint.Inject("SetGlobalIndexVersion", func(val failpoint.Value) {
if valInt, ok := val.(int); ok {
tbInfo.Indices[idxOffset].GlobalIndexVersion = uint8(valInt)
}
})
}
} else {
tbInfo.Indices[idxOffset].Global = false
}
Expand Down Expand Up @@ -3293,6 +3303,16 @@ func (w *worker) onReorganizePartition(jobCtx *jobContext, job *model.Job) (ver
tblInfo.Partition.DDLChangedIndex[index.ID] = false
tblInfo.Partition.DDLChangedIndex[newIndex.ID] = true
newIndex.Global = newGlobal
newIndex.GlobalIndexVersion = 0
if newGlobal && !newIndex.Unique && !tblInfo.HasClusteredIndex() {
// Only use V2 for non-clustered tables with non-unique global indexes
newIndex.GlobalIndexVersion = model.GlobalIndexVersionV2
failpoint.Inject("SetGlobalIndexVersion", func(val failpoint.Value) {
if valInt, ok := val.(int); ok {
newIndex.GlobalIndexVersion = uint8(valInt)
}
})
}
tblInfo.Indices = append(tblInfo.Indices, newIndex)
}
failpoint.Inject("reorgPartCancel1", func(val failpoint.Value) {
Expand Down
1 change: 1 addition & 0 deletions pkg/ddl/tests/partition/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go_test(
"db_partition_test.go",
"error_injection_test.go",
"exchange_partition_test.go",
"global_index_version_test.go",
"main_test.go",
"multi_domain_test.go",
"placement_test.go",
Expand Down
Loading
Loading