matrixorigin / matrixorigin/matrixone

[Bug]: concurrent ADD FOREIGN KEY can publish a constraint over an orphan row

Open
#28,934 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

`ALTER TABLE ... ADD FOREIGN KEY` can publish a foreign-key constraint while a concurrent successful `UPDATE` has already created an orphan child row. The published constraint rejects later orphan writes, but the first orphan remains committed.

## Environment

- Branch: `main`
- Commit: `b347a62a30719cf3f2c8ca5a2da0dcfb11639f8c`
- Deployment: isolated local 1 LogService + 1 TN + 2 CN deployment; the update and DDL used independent direct CN SQL endpoints.
- Date: 2026-09-15

## Steps to reproduce

```sql
CREATE TABLE p(id INT PRIMARY KEY);
CREATE TABLE c(id INT PRIMARY KEY, pid INT NOT NULL);
INSERT INTO p SELECT result FROM generate_series(1, 2000);
INSERT INTO c SELECT result, result FROM generate_series(1, 2000);
```

Submit these through two CNs at the same time:

```sql
-- CN 1
SET foreign_key_checks = 1;
UPDATE c SET pid = 0 WHERE id = 501;

-- CN 2
SET foreign_key_checks = 1;
ALTER TABLE c ADD CONSTRAINT fk_cp FOREIGN KEY(pid) REFERENCES p(id);
```

## Actual behavior

Both statements return success in affected generations. The catalog reports the foreign key, but the child table contains the orphan row:

```sql
SELECT c.id, c.pid, p.id IS NULL AS orphaned
FROM c LEFT JOIN p ON c.pid = p.id
WHERE c.id = 501;
-- 501, 0, 1

SHOW CREATE TABLE c;
-- ... CONSTRAINT `fk_cp` FOREIGN KEY (`pid`) REFERENCES `p` (`id`) ...
```

A later `INSERT INTO c VALUES (10001, 0)` is rejected with the foreign-key error. The original orphan row is therefore an integrity violation retained under the published constraint.

Three independent fresh generations reproduced the state:

- 2,000 rows: run 57 of 200;
- 10,000 rows: run 153 of 300;
- 2,000 rows: run 55 of 500.

The last preserved generation was copied by snapshot clone and data branch; source, clone, and branch each retained one orphan row when joined to the unchanged parent table.

## Expected behavior

The DDL and update must serialize. If the update commits first, foreign-key creation must reject the existing orphan. If foreign-key creation commits first, the update must be rejected. Both statements must not succeed while an orphan child is retained.

## Controls

- Sequential `ADD FOREIGN KEY` then invalid `UPDATE`: 20/20 left a published constraint and no orphan row; the update was rejected.
- Sequential invalid `UPDATE` then `ADD FOREIGN KEY`: 20/20 left one orphan and no published constraint; the DDL was rejected.
- MySQL 8.0.45 concurrent comparison: 50 fresh generations, no generation published the foreign key while retaining an orphan. It produced only the two serializable outcomes.

## Evidence

Local redacted drivers and outputs are retained under `evidence/ddl_cross_complete/`:

- `add_foreign_key_conflicting_update_race.sh`
- `add_foreign_key_conflicting_update_race_b347.out`
- `add_foreign_key_conflicting_update_preserve_b347.out`
- `add_foreign_key_conflicting_update_third_b347.out`
- `add_foreign_key_conflicting_update_preserved_queries_b347.out`
- `add_foreign_key_orphan_lifecycle_b347.out`
- `add_foreign_key_conflicting_update_control_b347.out`
- `mysql_add_foreign_key_conflicting_update_race_8.0.45.out`

## Code analysis

The exact root cause is not yet confirmed. This is a cross-CN DDL/DML commit-order failure: the foreign-key metadata becomes visible without validating or blocking the concurrently committed child-row mutation.

## Regression coverage

Do not baseline the orphan state. After a fix, add a repeated dual-CN scenario to MOTR that concurrently adds a foreign key and changes a child key to a nonexistent parent, then asserts that every terminal state is serializable and validates source/clone/branch after the operation. The minimal fixture needs neither big-data nor chaos coverage.

## Related

- #28931 covers concurrent `CREATE INDEX` publishing an index that omits a successful update. This issue concerns foreign-key enforcement and a catalog constraint published over an orphan child row.
- #26804 is closed and concerns a different failed-unique-DDL corruption path.

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.