CONCEPT Cited by 1 source
Vitess foreign-key enforcement¶
Definition¶
Vitess foreign-key enforcement is the layer in Vitess that takes over from InnoDB for any DML that interacts with a foreign-key constraint with a cascading action. Instead of letting the underlying MySQL server enforce the FK natively, VTGate analyses the statement against the schema's FK graph and issues the explicit child-first / parent-second operations (plus locking) required to record every cascaded child change in the binlog — which MySQL would otherwise hide inside InnoDB (see concepts/innodb-silent-cascade-in-binlog).
Why this exists¶
MySQL/InnoDB's native FK support has one architectural property that is incompatible with everything above it in Vitess that depends on the binlog: cascaded child writes never reach the binlog. Reprising the list of dependent subsystems:
- VReplication (data motion, resharding, online DDL, online imports, CDC).
- Replication-based Online DDL (shadow tables rely on binlog tail for catch-up).
- VStream CDC consumers (Debezium, Airbyte, PlanetScale Connect, Fivetran).
- Any external replication or analytics consumer.
PlanetScale chose Vitess owns FK logic over patch InnoDB (see concepts/innodb-silent-cascade-in-binlog for the four-reason trade-off).
What cases VTGate actually intercepts¶
ON [DELETE|UPDATE] RESTRICT (aka NO ACTION)¶
Delegate to MySQL. No cascade, so no binlog hole. MySQL's native enforcement rejects the parent write if any child row would be orphaned. VTGate stays out of the way. Canonical fastpath.
ON DELETE CASCADE¶
VTGate orchestrates explicitly via patterns/application-level-cascade-orchestration:
SELECT col FROM parent WHERE id=7 FOR UPDATE— lock the parent rows and read their FK column values.DELETE FROM child WHERE col IN (<parent_values>)— binlog the child-row deletes explicitly (the step InnoDB would skip).- Recurse for grandchildren if present.
DELETE FROM parent WHERE id=7— InnoDB sees no matching children, nothing to cascade.
All four steps in a single transaction. VTGate acquires more locks than MySQL would; it also emits more binlog events — one per child row explicitly, instead of just the parent.
ON UPDATE CASCADE¶
More complex, because the pre-update child row doesn't match
any parent row with the new key, but does match with the
old key. Vitess must update the child before the parent
— which requires turning off FOREIGN_KEY_CHECKS for the
statement:
SET FOREIGN_KEY_CHECKS=0;
UPDATE child SET parent_id=8 WHERE parent_id=7;
UPDATE parent SET id=8 WHERE id=7;
SET FOREIGN_KEY_CHECKS=1;
Doing this disables all MySQL FK checks for the update,
including RESTRICT on any grandchild. Vitess therefore
must re-validate those grandchild RESTRICTs in application
code before issuing the update. That validation is itself a
subtle surface — see the fuzzer bug #1 in the source post
(sources/2026-04-21-planetscale-the-challenges-of-supporting-foreign-key-constraints)
where a no-op update was incorrectly rejected because the
validation JOIN didn't exclude unchanged rows.
ON DELETE SET NULL / ON UPDATE SET NULL¶
Similar to CASCADE: child must be updated first (set the FK
column to NULL), then the parent operation proceeds. Binlog
captures both explicitly.
Scope: single-shard only (as of 2023-12-05)¶
All of the above applies only to unsharded / single-shard databases. Multi-shard FKs (shard-scoped, or cross-shard) are roadmap. From the post: "foreign key constraint support is limited to unsharded/single shard databases. We expect to support shard-scoped foreign key constraints in a multi-shard environment, or even cross-shard foreign keys, but we limit the current discussion to a single shard." The Vitess-owns-FK architectural choice is partially motivated by the cross-shard future, but the implementation lags the architecture.
Cost of Vitess-level FK enforcement¶
- More locking than native MySQL —
SELECT ... FOR UPDATEon the parent before issuing child DMLs. - More round-trips between VTGate and the backing MySQL —
at least one extra
SELECT+ one extra child-side DML per FK depth level. - Non-zero performance impact framed in the post. No specific numbers disclosed.
Vitess's normal design philosophy for FK-free workloads is to delegate queries directly to the backing MySQL for performance; for FK workloads, that optimisation is consciously traded off for correctness + CDC-visibility.
Gotchas surfaced by the fuzzer¶
Four bugs identified by the random-DML fuzzer and fixed in patches, illustrating the subtlety of FK semantics above MySQL:
- No-op update + validation JOIN —
UPDATE parent SET col=2 WHERE col=2(row doesn't actually change) was incorrectly rejected by Vitess's validationJOINunder CASCADE+RESTRICT chaining. Fix: exclude unchanged rows from the validation predicate (AND child.col NOT IN (2)). - Arithmetic on
VARCHARreturning-0 FLOAT— Vitess's cascade-SELECT returned-0forcol * (col - (col))on a varchar column; MySQL normalised-0 == 0when applied back to the child. Fix: CAST the expression to the target column's type (CAST(... AS CHAR)). - Missing gap locks on
REPLACE INTO+ unique index — cascadeSELECT ... FOR UPDATEwasn't gap-locking on a unique key, allowing a concurrent insert to slot in between select and cascade. Fix: promote toNOWAITto fail-fast instead of block — see patterns/nowait-lock-for-cascade-select. - Cyclic FKs across different tables — prohibited altogether (see concepts/cyclic-foreign-key-prohibition); self-reference remains allowed.
Seen in¶
- sources/2026-04-21-planetscale-the-challenges-of-supporting-foreign-key-constraints
— canonical wiki home for the concept. Noach + Gupta walk
the per-action logic with worked examples (
DELETE FROM parent WHERE id=7,UPDATE parent SET id=8 WHERE id=7), name the trade-offs, and trace the four representative fuzzer bugs to their mechanism-level fix.