I got a question last week about conflict resolution. The user had read Programmatic Data Conflict Resolution and Three-Way Merge in a SQL Database and was left wanting more. Both posts use the same line:
Resolving conflicts is up to the user.
The user wanted to know what users actually do. This article is my answer.
What Is a Conflict?#
A data conflict happens when Dolt’s three-way merge can’t auto-resolve. The same row-column pair was modified on both branches since they last shared an ancestor, and Dolt doesn’t know which version should win. Dolt surfaces the conflict in the dolt_conflicts_$table system table with three versions of the row: base, ours, and theirs. From there, you decide what to do. You can resolve manually, with stored procedures or with programmatic resolution logic.
Dolt also surfaces schema conflicts. The conflict detection rules are complicated and even rarer that data conflicts. Schema conflicts generally will be manually resolved and thus, we’re going to ignore them in this article to focus on data conflicts.
Here’s a data conflict end-to-end with the dolt CLI:
# Create a new dolt database
dolt init
# Create a simple table and commit
dolt sql -q "CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))"
dolt sql -q "INSERT INTO users VALUES (1, 'Tim')"
dolt add users
dolt commit -m "create users table with Tim"
# Branch off and insert a row on the new branch
dolt checkout -b feature
dolt sql -q "INSERT INTO users VALUES (2, 'Brian')"
dolt add users
dolt commit -m "add Brian on feature"
# Switch back to main and insert a conflicting row at id=2
dolt checkout main
dolt sql -q "INSERT INTO users VALUES (2, 'Aaron')"
dolt add users
dolt commit -m "add Aaron on main"
# Merge feature into main - this creates a conflict at id=2
dolt merge feature
# Inspect the conflict
dolt sql -q "SELECT * FROM dolt_conflicts_users" -r vertical
That last query returns the conflicting row in three flavors: base, ours, and theirs.
*************************** 1. row ***************************
from_root_ish: 9sfq8g8glasdfr99sgal3634tiec1jfe
base_id: NULL
base_name: NULL
our_id: 2
our_name: Aaron
our_diff_type: added
their_id: 2
their_name: Brian
their_diff_type: added
dolt_conflict_id: e16MB7H3mK83nM037FTWlw
base_id and base_name are NULL because both branches added id=2 independently. There’s no common ancestor row. our_name is Aaron from main, their_name is Brian from feature. Now, what do you do? It’s up to the user.
Most Conflicts Never Happen#
Before explaining the conflict resolution patterns, here’s the boring truth: most of our users experience conflicts very rarely. Engineers design their systems so conflicts don’t happen. Schema is partitioned by writer. Branches are short-lived. Single writer per row. autoincrement counters are shared between branches. A conflict is an exception, not a common case.
This matters because elaborate business logic to resolve a conflict is real but rare. This is the truth behind “resolving conflicts is up to the user”. Some users run Dolt in production for years and hit a conflict a handful of times. Most users treat conflict resolution like exception handling: have a plan, but optimize the happy path.
For the human-editing-data case on DoltHub, conflicts get resolved through the DoltHub user interface. This is the same flow as a merge conflict on GitHub. A human looks, decides, and clicks. This article isn’t about that case.
This article is about the automated case. Your application hits a conflict in the middle of the night. What happens? In these cases we see three common patterns:
- Bail
- Note and Continue
- Resolve
1. Bail#
Oops. That’s a conflict. Something bad happened. Rebase from main, redrive my change off the latest, try again.
This is by far the most common approach. The application catches the conflict, rolls back, pulls tip, and reapplies its change against fresh data. The equivalent of “Oh shit, I didn’t realize I was working off an old commit. I need to grab tip and redo my changes.” Optimistic concurrency, but with branches. You are relying on existing application logic to handle the conflicting write on the second pass.
The difference between this approach and optimistic concurrency is you can leave the conflicting branch around and debug the conflict later. The data is not destroyed. Put a conflict error in the logs and pick the issue up when you’re ready. Wait for a handful of conflicts to occur so you can see some patterns. Leverage the power of version control to make your application more durable against conflicting writes.
2. Note Conflict and Continue#
Some users don’t bail. They commit one side of the merge in the moment, usually “ours”, and let the conflict sit in dolt_conflicts_$table as a record that something weird happened. This is not the default Dolt option. You must enable this workflow with the system variable @@dolt_allow_commit_conflicts.
Then, a nightly-ish post process comes around and decides what to do. Maybe it picks the other side. Maybe it makes an issue for a human. Maybe it just logs the divergence and moves on. This is the formal method for the “leave an unmerged branch with conflicts” strategy above. You can embed complicated business rules for conflicts here, including escalating to a human if necessary.
I think of this as “offline conflict resolution”. The conflict is a signal, not a blocker. The transaction succeeded with one side picked, similar to a traditional transaction rollback that you can still inspect later. The history has a complete record of what diverged. Cleanup happens out of band.
This pattern works when moving forward in the moment matters more than perfect data, when you have enough conflicts that batching pays off, or when you have a review process that can run async.
3. Resolve#
Finally, you can write code to fix the conflict at merge time. You write business logic that decides what to do with the conflicting merge. Query dolt_conflicts_$table, look at the base, ours, and theirs, and pick. Maybe newest timestamp wins. Maybe you sum the diffs. Maybe you ask the user.
This is the least popular approach I see in the wild.
The reason is most users haven’t had to build this kind of business logic before. Their previous systems didn’t surface conflicts. With Dolt, they suddenly have visibility into a class of problem they used to silently lose data on. The first instinct is “keep the history, don’t try to be too clever”. They reach for #1 or #2.
So, you can run #1 or #2 for a while, start to see patterns and build business logic to resolve certain classes of conflict correctly in the moment. This is how you get to #3.
Do Agents Help?#
The same user also had two more pointed questions about agents and conflicts:
- Are agents resolving conflicts now?
- How do we handle probabilistic agents playing judge?
Honestly, I haven’t seen anyone build this yet. It’s only been a few months of agents writing code and most people aren’t even dreaming of agents writing to databases, because traditional databases are not version controlled. But I think it’s coming to Dolt soon.
The argument is simple. A conflict is a three-row decision: base, ours, theirs. An agent with context about your business can almost certainly make a better call than a hardcoded “take the most recent timestamp” rule.
If I were building this today, I would do the following:
- Have the agent propose a resolution into a staging table, not the live row.
- Have the agent justify the proposal in plain English in the same commit.
- Have a human or a higher-trust reviewer agent merge the proposal before it goes live.
- Keep the entire chain — conflict, proposal, justification, approval — in commit history.
If you later find out the agent picked wrong, dolt revert and try again with more context.
So agents resolving conflicts may be coming to a database near you really soon.
Conclusion#
Users use three strategies to manage conflicts in Dolt: bail, note and continue, and resolve. This is a new area that generally requires innovation. Agents might help this process in the future but not yet. Questions about conflicts? Come by our Discord and I’ll be happy to answer.
