Database branching now creates an isolated, full copy of a production dataset in under a second, on Neon, PlanetScale, lakeFS and Dolt alike. What none of them do reliably is merge that branch back: schema diffing exists in limited form, but automatic reconciliation of divergent data, across row identity, foreign keys, and constraints, remains an unsolved problem every vendor quietly routes around.
That gap matters because branching is being sold as a git workflow for data, and git's whole value proposition rests on the second half: you branch so you can later merge with confidence. Take that away and we are left with a very fast way to create divergence, and no correspondingly fast way to resolve it.
Copy-on-write made the creation half trivial. Neon streams Postgres WAL into a disaggregated storage tier, so a new branch just references the parent's existing pages and only writes new ones when data actually changes, which is why branch creation completes in under a second regardless of whether the parent holds a gigabyte or a terabyte.
lakeFS does the analogous thing at the object-store layer: branches are pointers into commits, not physical copies, which is how lakeFS avoids duplicating data even at multi-petabyte scale, branching instantly regardless of how large the underlying lake is.
Dolt takes a different route to the same result. It chunks tables into roughly 4KB sections using Prolly Trees, so identical chunks across branches are stored exactly once. That structural sharing is, per Dolt's own writeup, the thing that lets branching scale past a handful of environments instead of collapsing into full-copy sprawl once you're running thousands of ephemeral branches.
None of that engineering touches merge. Git's merge works because source files are line-oriented text with no referential integrity to preserve. Database rows aren't like that: a row has identity that may be synthetic or reused across branches, foreign keys create dependency chains where merging a child requires its parent to already exist, and unique or check constraints that each branch individually satisfies can conflict the instant you combine them.
Diff granularity compounds this: the level at which a system can diff is generally the level at which it can merge, and coarse, table-level diffs mean two branches touching the same table conflict even when they never touch the same row.
Results vary sharply, and the variance is the story. Neon's branching documentation does not mention merge at all. Turso is explicit that you're on your own: "you need to handle merging any schema changes or data manually." PlanetScale's data branches don't sync data back to production under any circumstance, full stop.
| Platform | Branch creation | Data-level merge | Schema-level merge | Scope |
|---|---|---|---|---|
| Neon | Instant, WAL/page-level CoW | Not supported | Not supported | Postgres only |
| PlanetScale | Full copy (Vitess-managed) | Not supported | Three-way schema diff (Vitess schemadiff) | MySQL, schema-only |
| Turso | Instant | Manual only | Manual only | libSQL/SQLite |
| lakeFS | Instant, pointer-based | Commit/object-level only | N/A | Any object store, petabyte scale |
| Nessie | Instant | Commit-level only | N/A | Iceberg tables only |
| Dolt | Prolly-tree CoW | Row-level, git-like | Row-level (schema treated as data) | Requires Dolt-native storage |
No. It merges schema, and only schema. The algorithm computes two diffs, main-to-branch-A and main-to-branch-B, then tests applying both orders and flags a conflict if either application fails or the two orderings produce different results.
It's built on Vitess's schemadiff library doing semantic SQL comparison rather than textual diffing, which is a real engineering advance over naive DDL replay. It still has nothing to say about the rows inside those tables.
The merge check runs in four steps:
diff1, the schema delta from main to branch A.diff2, the schema delta from main to branch B.diff1 then diff2, and separately apply diff2 then diff1, against a copy of main.The subtlety here is worth naming because it is the kind of bug that survives code review. If branch A adds a column and branch B adds a different column to the same table, PlanetScale flags it as conflicting, correctly, because a SELECT * with positional argument binding would resolve columns 3 and 4 differently depending on merge order.
Index ordering, by contrast, is intentionally ignored. That's a deliberate, documented tradeoff. It is also proof that even the most sophisticated schema-merge implementation in this space stops at the schema boundary.
Here's where the git analogy actively misleads us when we build CI workflows around it. Teams wire up per-PR preview branches on Neon through GitHub Actions or the Vercel integration, and the branch gets created and destroyed automatically as PRs open and close. That part works well.
What breaks is cost hygiene, and it is a direct consequence of how branch billing works, as we found. But storage bills at roughly $0.35/GB-month even while compute is fully suspended, and compute bills per CU-hour rather than per wall-clock hour, a metering model that punishes workloads sized like a fixed environment rather than a metered one.
Preview branches are the common failure mode: spun up automatically per PR, they're easy to leave running long after the PR merges, and each one keeps billing as an independent database until someone tears it down.
Write amplification is the other leak, and it shows up as a measurable performance number rather than an abstract caveat. On ZFS-backed copy-on-write setups, Postgres's 8KB page size against ZFS's default 128KB record size produces up to 16x write amplification.
Tuning the record size helps proportionally. Disabling full_page_writes in one benchmark took throughput from 6,000 to 10,325 TPS, a roughly 70% gain, which tells you the default configuration was leaving real performance on the table specifically because of branching's storage model.
At performance certification, the git analogy stops holding. Xata states this directly: a branch is the right place to verify a migration completes without error, and the wrong place to certify a latency SLO. Isolation guarantees correctness testing, not production-equivalent load behavior, and conflating the two is an easy mistake we make once branch creation feels as cheap and instant as it does.
Because that is exactly where merge conflicts compound fastest and where teams are most tempted to skip staging in favor of a "just branch it" workflow. The longer two branches diverge before merging, the higher the odds of conflict, and high-write OLTP tables diverge fast by definition.
lakeFS and Nessie sidestep this by scoping merge to the commit or object level rather than the row level, which works for immutable lakehouse files but does not generalize to a customers table taking writes every second. Nessie in particular is scoped to Iceberg tables only, so it can't version arbitrary mutable state the way a relational branch tool implies it should.
Dolt is the one system in this comparison that merges data, not just schema, because its Prolly Tree structure gives it row-level, content-addressed diffing analogous to git's line-level diffing. The cost is scope: Dolt only works if your data lives inside Dolt's own storage format, so it is a poor fit for teams whose data already sits in Postgres, MySQL, or an existing lakehouse and isn't moving.
The frontier of the problem is now visible in academic work rather than vendor blogs. A recent paper, GitLake, proposes lifting single-table Iceberg snapshots into lakehouse-wide commits, branches, and merges, explicitly because per-table snapshotting does not compose across a multi-table warehouse.
It is motivated by agentic pipelines, where multiple automated writers work isolated branches concurrently and a human reviews before an atomic, all-or-nothing publish. The authors back their design with a preliminary Alloy formal model of the merge abstraction, which is a tell in itself: correctness of branch-merge semantics is still open enough that people are reaching for formal verification rather than shipping and hoping.
Branching is still a genuinely useful primitive for what we've found it's actually good at: throwaway environments, migration dry runs, PR previews. It stops being a git analogy the moment two branches need to become one again, and every platform in this space still asks a human to do that part by hand.