vitessio/vitess · error
VT12002
VT12002
Error message
VT12002(sqlparser.String(tblName), fk.Table.String())
What it means
VT12002 (unsupported: cross-shard foreign key constraint) is raised when a DELETE on a parent table has a child foreign key with ON DELETE RESTRICT that Vitess must emulate itself — specifically the cross-shard/cross-keyspace RESTRICT cases that reach createFkCascadeOpForDelete. Since RESTRICT semantics (reject if children exist) cannot be enforced efficiently across shards in this code path, the planner rejects the delete.
Solutions
- Change the FK to ON DELETE CASCADE or ON DELETE SET NULL if business logic allows, so Vitess can emulate it.
- Enforce the delete guard in application logic: check for and delete/NULL child rows before deleting the parent.
- Co-locate parent and child tables in the same keyspace/shard (via VReplication move or redesign) so MySQL can enforce RESTRICT natively.
- Set foreign_key_mode appropriately (e.g. `disallow` to reject cross-shard FK DDL up front) so the situation is surfaced during migration rather than at delete time.
Example fix
// before CREATE TABLE child (id INT, parent_id INT, FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE RESTRICT); // after CREATE TABLE child (id INT, parent_id INT, FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE);
Defensive patterns
Strategy: validation
Validate before calling
-- detect unsupported RESTRICT child FKs on a parent table before issuing DELETE SELECT child.table_name AS child_table FROM information_schema.referential_constraints rc JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = rc.constraint_name WHERE rc.unique_constraint_schema = ? AND rc.referenced_table_name = ? AND rc.delete_rule = 'RESTRICT';
Type guard
func fkDeleteIsSupported(fk vindexes.ChildFKInfo) bool {
return !fk.OnDelete.IsRestrict()
} Try / catch
if err := execDelete(sql); err != nil {
if strings.Contains(err.Error(), "VT12002") {
return fmt.Errorf("cross-shard RESTRICT FK blocks this DELETE; delete child rows first or use CASCADE: %w", err)
}
return err
} Prevention
- Prefer ON DELETE CASCADE or SET NULL in sharded schemas
- Delete child rows explicitly before parent rows when RESTRICT is required
- Avoid cross-keyspace foreign keys in sharded deployments
- Choose the right --foreign_key_mode at deployment and enforce it in schema review
When it happens
Trigger: DELETE from a parent table that has a child FK with `ON DELETE RESTRICT` where parent and child are in different keyspaces or the FK is managed by Vitess (managed foreign key mode) rather than pushed down to MySQL.
Common situations: Applications with legacy schemas using ON DELETE RESTRICT migrated to sharded Vitess; cross-keyspace FK relationships; enabling `--foreign_key_mode=managed`/`unmanaged` boundaries where RESTRICT cannot be honored sharded.
Related errors
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/77f86a46da7f822a.
Report an issue: GitHub.
Appendix: source
Thrown at go/vt/vtgate/planbuilder/operators/delete.go:351
tbl.Table, _ = tbl.Alias.TableName()
}
return op, Rewrote("change query table point to source table")
}, func(operator Operator) VisitRule {
_, ok := operator.(*QueryGraph)
return VisitRule(ok)
})
return vTbl
}
func createFkCascadeOpForDelete(ctx *plancontext.PlanningContext, parentOp Operator, delStmt *sqlparser.Delete, childFks []vindexes.ChildFKInfo, deletedTbl *vindexes.BaseTable) Operator {
fkChildren := make([]*FkChild, 0, len(childFks))
var selectExprs []sqlparser.SelectExpr
tblName := delStmt.Targets[0]
for _, fk := range childFks {
// Any RESTRICT type foreign keys that arrive here,
// are cross-shard/cross-keyspace RESTRICT cases, which we don't currently support.
if fk.OnDelete.IsRestrict() {
panic(vterrors.VT12002(sqlparser.String(tblName), fk.Table.String()))
}
// We need to select all the parent columns for the foreign key constraint, to use in the update of the child table.
var offsets []int
offsets, selectExprs = addColumns(ctx, fk.ParentColumns, selectExprs, tblName)
fkChildren = append(fkChildren,
createFkChildForDelete(ctx, fk, offsets))
}
selectionOp := createSelectionOp(ctx, selectExprs, delStmt.TableExprs, delStmt.Where, nil, nil, getUpdateLock(deletedTbl))
return &FkCascade{
Selection: selectionOp,
Children: fkChildren,
Parent: parentOp,
}
}
View on GitHub (pinned to 01a25a7d17)