matrixorigin / matrixorigin/matrixone

[Bug]: concurrent CREATE INDEX accepts duplicate names and creates ambiguous index metadata

Open
#28,928 1 comment 0 reactions 1 assignee Claimed by @aptend View on GitHub
kind/bug needs-triage
Dominant language
Go
Stars
1.9k
Forks
311
Avg merge
1d 3h
Merged PRs (30d)
768

Description

## Description

Two concurrent `CREATE INDEX` statements using the same logical index name can both return success. When the statements reference different columns, MatrixOne keeps two physical hidden indexes under that one logical name, while different metadata surfaces disagree about the table definition.

## Environment

- MatrixOne official `main`: `ceaf9c6f88ab184d4e1684d413bfe40ae4b56a44`
- Topology: local 2 CN / 1 TN / 1 Log; each DDL sent directly to a different CN
- Date: 2026-09-15

## Reproduction

```sql
CREATE DATABASE idx_race;
USE idx_race;
CREATE TABLE t(id BIGINT PRIMARY KEY, v INT, w INT);
```

Execute the following statements concurrently on two CNs:

```sql
-- CN1
CREATE INDEX ix_diff ON t(v);

-- CN2
CREATE INDEX ix_diff ON t(w);
```

After both statements return success:

```sql
SHOW CREATE TABLE t;
SHOW INDEX FROM t;

SELECT name,column_name,ordinal_position,index_table_name
FROM mo_catalog.mo_indexes
WHERE table_id=(
SELECT rel_id FROM mo_catalog.mo_tables
WHERE reldatabase='idx_race' AND relname='t'
)
ORDER BY name,column_name,index_table_name;
```

## Actual behavior

Both clients receive success. The resulting metadata is ambiguous:

- `SHOW CREATE TABLE` displays only `KEY ix_diff (v)`;
- `SHOW INDEX` displays two `ix_diff` rows, both at `Seq_in_index=1`, one for `v` and one for `w`;
- `mo_catalog.mo_indexes` contains two different hidden secondary-index tables whose logical name is `ix_diff`;
- `FORCE INDEX(ix_diff)` can route predicates on either `v` or `w` through an index-table scan;
- one `ALTER TABLE t DROP INDEX ix_diff` removes both physical indexes.

The same-name/same-definition race also lets both clients return success, although a sequential second `CREATE INDEX` correctly returns a duplicate-key-name error.

## Expected behavior

The logical index name must remain unique. After serialization on the table DDL lock, one concurrent statement should succeed and the other should return the same duplicate-name error as a sequential duplicate. No second hidden index or conflicting catalog entry should be committed.

## Reproducibility and controls

- Different definitions (`ix_diff(v)` vs `ix_diff(w)`): 5/5 fresh MatrixOne databases had both statements succeed and retained both physical indexes.
- Identical definitions (`ix_same(v)` vs `ix_same(v)`): 10/10 earlier fresh generations and 5/5 focused generations had both statements succeed.
- Sequential duplicate: rejected in every MatrixOne control.
- MySQL 8.0.45 control: 5/5 identical-definition and 5/5 different-definition races produced exactly one success and one duplicate-name failure, with one winning index definition.
- Base rows and both physical indexes remained readable, so this is not merely cross-CN visibility delay; the duplicate logical ownership is durably present in catalog state until DROP.

## Relevant implementation path

`buildCreateIndex` checks `tableDef.Indexes` for a duplicate name while building the plan. `Scope.CreateIndex` later acquires the exclusive table DDL lock, but `doCreateIndex` executes the already-built plan without revalidating the logical name against the table definition visible after the lock is obtained. Two plans built from the same pre-index schema can therefore serialize their commits while both retain a stale “name available” decision.

Relevant code on this commit:

- `pkg/sql/plan/build_ddl.go` (`buildCreateIndex` duplicate-name check)
- `pkg/sql/compile/ddl.go` (`Scope.CreateIndex` and `doCreateIndex`)

## Regression direction

Add a deterministic two-CN concurrency case with a barrier before commit. Cover both identical and different column definitions, require exactly one success, and assert one logical name, one hidden index owner, a single `SHOW INDEX` definition, correct lookup results, and clean single-index DROP behavior.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.