Skip to content

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:

  1. Compute diff1 = diff(main, branch1) and diff2 = diff(main, branch2) using a structural diff library (schemadiff).
  2. Run the commutativity check: compose both diffs in both orders; verify both compositions are valid and produce equal final schemas.
  3. If the check fails → conflict; reject or warn.
  4. 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:

  1. 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.
  2. In-memory simulation is cheap. schemadiff applies 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.
  3. 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. schemadiff is open-source through Vitess, but a non-Vitess platform must build or adopt equivalent.
  • MySQL-specific semantics. schemadiff models 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

Last updated · 550 distilled / 1,221 read