matrixorigin / matrixorigin/matrixone
[Bug]: concurrent ADD FOREIGN KEY can publish a constraint over an orphan row
- 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
Assessment
This issue has not been assessed yet.