Skip to content

PlanetScale — Database branching: three-way merge for schema changes

Summary

Shlomi Noach (PlanetScale / Vitess) describes three-way merge for schema changes — the mechanism PlanetScale uses to detect conflicts between concurrent deploy requests submitted against the same production branch. The concept is lifted from Git's three-way merge but executed on semantic SQL diffs (ALTER TABLE …, CREATE TABLE …) rather than textual patches. The core test is diff composition commutativity: for two diffs diff1 = diff(main, branch1) and diff2 = diff(main, branch2), the merge is conflict-free iff both diff1(diff2(main)) and diff2(diff1(main)) are valid and equal. When they differ (column-order mismatch, for example) or either is invalid (two columns with same name, different types), the system reports a conflict. Identical overlapping changes (both branches add the same name column) are recognised and auto-adapted rather than flagged. The algorithm runs at deploy-request submission time, giving developers an early warning rather than discovering the conflict after hours of queue wait.

Key takeaways

  1. Three-way merge terminology is explicitly borrowed from Git, but implementation is completely different. Git merges text; schema merge operates on SQL DDL statements generated by Vitess's schemadiff library.

    "It's similar in concept, but completely different in implementation." (Source: sources/2026-04-21-planetscale-database-branching-three-way-merge-for-schema-changes)

  2. The semantic SQL diff replaces textual diff. The raw git diff of two CREATE TABLE definitions is not executable against a database. PlanetScale instead emits an equivalent ALTER TABLE … ADD COLUMN … statement — the semantic diff that can actually run. The textual diff is replaced by "a semantic SQL diff" throughout the deploy flow.

  3. Conflict detection as function composition. Noach formalises: "Compute diff1 as diff(main, branch1) … We can consider diff1 as a function — i.e., diff1(main) => branch1. Likewise, compute diff2 as diff(main, branch2)." The three-way-merge test is then:

  4. If diff1(diff2(main)) is invalid → conflict.

  5. If diff2(diff1(main)) is invalid → conflict.
  6. If both are valid but diff1(diff2(main)) != diff2(diff1(main)) → conflict.
  7. If both are valid and equal → no conflict.

This is a commutativity check over diff composition.

  1. Column-order conflicts are real. Both branches add different columns to customer, both as the last column. "The order of columns in a table matters. Queries that run a SELECT * FROM customer and use positional arguments will get different columns at positions 3 and 4. The two branches conflict with each other. This is similar to a Git merge conflict where two branches append different rows to the end of a file." This is the column-order conflict — semantically equivalent changes still conflict because MySQL cares about physical ordering. Fix: one branch must anchor the new column with AFTER <existing-column> to make ordering deterministic.

  2. Index ordering is explicitly disregarded. The same commutativity logic would flag a conflict when two branches add indexes in different orders:

    "The only change is the output of SHOW CREATE TABLE as well as INFORMATION_SCHEMA introspection. PlanetScale disregards index ordering." Design judgement: even though diff1(diff2(main)) != diff2(diff1(main)) on index order, the execution semantics of every query are unchanged — so the commutativity check exempts index order. This is a worked example of operator judgement overriding the naive mathematical check.

  3. Identical partial overlap is auto-adapted, not flagged. When branch1 and branch2 both add the same name column (plus unrelated other changes), the three-way-merge treats that as overlap:

    "Given that both branches completely agree on that particular change, PlanetScale's three-way merge considers this as an overlap and allows it. Should branch1 merge first, branch2's diff auto-adapts and is left to the creation of tbl2 only." Mechanism: schemadiff formalises each diff (ALTER, CREATE, …) individually, so matching sub-diffs can be set-subtracted from the second branch's diff set after the first merges.

  4. Early conflict warning at queue admission. "When a developer submits their deploy request, their change is validated against all queued changes. This avoids the situation where the developer waits for hours in queue, only to learn the one deployment before theirs caused a conflict. PlanetScale shoots an early warning so that developers can better use their time in queue." This is the early queue-admission warning — conflict is shifted left from cutover to submit.

  5. Nothing tracks intermediate branch state. "It's worth pointing out that nothing tracks the changes on a development branch while it's open. Dev 1 may CREATE, ALTER, and DROP all they want." The system only computes the diff when the deploy request is created, comparing the current branch schema to main. No replay of intermediate states is required — all logic is a pure function of two end states.

Systems

Concepts

Patterns

Numbers and operational details

  • The post does not disclose production numbers (N of concurrent branches per tenant, CPU/memory cost of the commutativity check, algorithmic complexity of the diff composition). It is a conceptual post walking through the algorithm with a customer-facing framing.
  • The worked examples use a customer(id, [name], [subscription_type], [joined_at]) table and a delivery(id, customer_id) table — canonical small-scale toy examples.
  • Algorithm is described as "more elaborate than described thus far" — the post presents a simplified core with named extensions (index-order exemption, identical-overlap auto-adapt, pre-queue conflict warning). Extensions beyond these are not enumerated.

Caveats

  • Not all conflicts are detected by mechanical commutativity. The post only mentions structural conflicts (column-name collision, column-order mismatch). Semantic conflicts that don't show up in DDL (e.g., a new NOT NULL column plus an old application-level assumption of nullable) are out-of-scope for schemadiff's check.
  • Index-ordering exemption is a design choice, not a mathematical property. PlanetScale chose to disregard index order because query semantics are unchanged. A different schema-diff tool could surface it as a conflict. This is a policy layer on top of the commutativity primitive.
  • The post frames three-way merge as a pre-deploy conflict detector, not a merge commit synthesiser. Unlike Git, the system does not produce a diff_merged = merge(diff1, diff2) that can be run once. The schemadiff-driven deploy pipeline runs each branch's diff separately, in series, in queue-submission order — with the three-way-merge check as admission control.
  • Applies to schema, not data. Data conflicts (two branches write different values to the same row) are outside scope. PlanetScale's branching model snapshots the schema, not the data; data merges are a separate (unsolved-in-this-post) problem.
  • Origin 2023-04-26, re-fetched 2026-04-21. The algorithm has been in production for roughly three years by the time of this wiki ingest; the post remains the canonical public description of the mechanism.

Source

Last updated · 550 distilled / 1,221 read