PATTERN Cited by 2 sources
Three-way merge for schema changes¶
Problem¶
In a branch-based database schema workflow
(patterns/branch-based-schema-change-workflow), many
developers fork their own working branches from main
and propose schema changes concurrently. Conflicts are
inevitable:
- Two branches add columns with the same name but different types.
- Two branches add columns in different orders.
- Two branches touch interrelated views and tables in ways that produce different end-states depending on merge order.
- Two branches both propose the same change (often following the same ticket / RFC).
Detecting these conflicts manually is error-prone. Waiting until cutover to discover them wastes developer time and queue capacity. The system needs a structured, automatic conflict test at deploy-request submission time.
Solution¶
Adopt Git's three-way-merge mental model and implement it over semantic SQL diffs instead of textual file diffs:
- Compute
diff1 = diff(main, branch1)anddiff2 = diff(main, branch2)using a structural diff library (schemadiff). - Run the commutativity check: compose both diffs in both orders; verify both compositions are valid and produce equal final schemas.
- If the check fails → conflict; reject or warn.
- If the check passes → queue the branch for normal deployment.
Layer two policy refinements on top of the pure mathematical check:
- Index-order exemption: ignore commutativity failures that only affect index ordering, because query semantics are unchanged.
- Overlap auto-adaptation: recognise identical sub-diffs shared between branches and let the first merge consume them, leaving the second branch's remaining diff unaffected by the shared portion.
Run the check at queue- admission time as an early warning so developers can act on conflicts while the context is fresh.
Why it works¶
Three structural properties make this tractable:
- Semantic diffs compose as functions. A
semantic schema diff
is a function
schema → schema. Function composition is defined; commutativity of composition is a well-formed test. - In-memory simulation is cheap.
schemadiffapplies DDL to an in-memory schema representation without touching the database. The check runs in milliseconds on typical schemas, fast enough for the deploy-request submit path. - Conflicts partition cleanly. Disjoint-entity changes always commute. Entity-overlapping changes either produce identical schemas (overlap) or different schemas (genuine conflict) or invalid intermediate states. These three cases are exhaustive.
Canonical framing¶
Shlomi Noach's introduction (2023, PlanetScale):
"You may be familiar with Git's three-way merge as a way to resolve source code changes made by developers on their independent branches. PlanetScale offers three-way merge for your schema branches, making schema change collaboration simpler and safer. It's similar in concept, but completely different in implementation." (Source: sources/2026-04-21-planetscale-database-branching-three-way-merge-for-schema-changes)
Implementation outline¶
on_deploy_request_submitted(request):
branch1 = request.branch
main = production_schema_at_fork_time(branch1)
diff1 = schemadiff(main, branch1)
for queued in current_queue():
diff2 = queued.semantic_diff
# Apply in both orders
schema_12 = simulate(diff1, simulate(diff2, main))
schema_21 = simulate(diff2, simulate(diff1, main))
# Check validity + equality, with policy exemptions
conflict = (
invalid(schema_12) or
invalid(schema_21) or
not equal_ignoring_index_order(schema_12, schema_21)
)
if conflict and not identical_overlap(diff1, diff2):
warn(request, queued)
admit(request)
The simulate step mutates an in-memory schema by
applying each DDL statement and re-validating. The
equal_ignoring_index_order comparator canonicalises
index sequence (since PlanetScale disregards index
order). identical_overlap checks whether matching
statements make the apparent conflict benign (since
auto-adaptation will handle it at cutover).
Consequences¶
Benefits¶
- Conflicts shift left. Developers learn about conflicts at submission time, not after hours of queue wait.
- No false textual positives. Whitespace, backtick placement, keyword case, etc., don't trigger conflicts because the comparison is on parsed DDL, not source text.
- No false textual negatives. A column-type change written in two slightly different SQL styles is still detected as the same change (or different, per actual semantics — not per textual form).
- Pluggable policy layer. Index-order exemption, overlap auto-adaptation, and other pragmatic exceptions are additions, not core to the commutativity test — easy to extend.
Costs¶
- Requires a semantic schema diff library. This is
a non-trivial piece of infrastructure.
schemadiffis open-source through Vitess, but a non-Vitess platform must build or adopt equivalent. - MySQL-specific semantics.
schemadiffmodels MySQL DDL rules; a Postgres variant requires rewriting the validation layer. - Quadratic in queue size for naive implementation. The check runs pairwise between the new request and each queued request. For very large queues, more sophisticated structures (conflict graph, batched checks) may be needed — the post doesn't discuss this.
- Not a data-level merge. Handles schema only. Conflicts in actual row data are out of scope.
When to apply¶
- Multi-tenant database platform with concurrent schema changes (PlanetScale, Vitess, gh-ost ecosystems).
- Any product offering database branching with deploy-request review flows.
- Schema-change CI pipelines that want to reject incompatible pull requests before they run.
When not to apply¶
- Single-developer databases — no concurrency, no benefit.
- Environments without a formal schema diff library — building one just for this check is overkill.
- Workloads where conflicts are rare enough that manual cutover-time detection is acceptable (small teams, infrequent schema changes).
Seen in¶
- sources/2026-04-21-planetscale-database-branching-three-way-merge-for-schema-changes
— canonical introduction by Shlomi Noach. The pattern
is described end-to-end (diff → commutativity test →
conflict/no-conflict), grounded in Vitess's
schemadifflibrary, with four worked examples (disjoint, name-collision, column-order, identical-overlap) and two policy exemptions (index-order, overlap).
Related¶
- concepts/schema-three-way-merge
- concepts/semantic-schema-diff
- concepts/diff-commutativity-check
- concepts/column-order-conflict
- concepts/diff-overlap-auto-adaptation
- concepts/queue-admission-conflict-warning
- concepts/database-branching
- concepts/deploy-request
- concepts/schema-change-queue
- concepts/pre-flight-schema-conflict-check
- patterns/branch-based-schema-change-workflow
- patterns/early-queue-conflict-warning
- systems/vitess-schemadiff
- systems/planetscale
-
sources/2026-04-21-planetscale-the-challenges-of-supporting-foreign-key-constraints — FK-graph dependency altitude. The same schemadiff engine that powers three-way merge also computes FK-topology-aware deploy paths (see concepts/schema-change-deploy-order). Canonical wiki framing: three-way merge's semantic-diff computation must understand the FK dependency graph to report conflicts correctly — two branches adding FKs to the same table are conflict-free only if their introduced FKs don't reference tables the other branch has dropped. First wiki datum that three-way merge's scope includes FK-dependency semantics, not just column-level differences.