Dolt is the world’s first SQL database with built-in version control features. This includes the ability to branch and merge. The natural consequence is that you can have merge conflicts.
When writing source code, the act of resolving a merge conflict is generally a manual process. This requires a human resolver to understand the intent of the code and make a decision on how to resolve the conflict.
Conflict resolution in data is a different story. When two branches have conflicting changes, the way to resolve the conflict is entirely application-specific. For example, if two branches have conflicting changes to a user’s email address, the resolution might be to take the most recent change. However, if two branches have conflicting changes to a user’s account balance, the resolution might be to take the sum of the two changes.
I should also mention that schema conflicts are a thing, but this blog is focused on data conflicts. Schema conflicts are a bit more complex, and we aren’t covering them here. Today is all about data conflicts. Let’s dive in.
Speed Cubing#
The world of Speed Cubing is truly remarkable. The world record for a standard 3x3 cube was recently broken by Teodor Zajder with a time of 2.76 seconds. The record was set in Gdańsk, Poland, on February 8, 2026. It’s a short video, you should watch it! (Sorry, couldn’t embed it.)
In professional speed cubing, the starting position of a given cube is determined by a scramble. A scramble is a sequence of moves that is applied to a solved cube to create a specific starting position. The scramble is defined by the World Cube Association, and for the world record mentioned above, the scramble was:
L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2
I won’t break the syntax down for you. I’m sure it’s a conversation you can have with an AI or something, but the point is that there is a well-defined way to identify a starting position of a cube. This enables people in the future to get to the same starting position and try it themselves, or talk about a particular solution.
I work at a database company, and let’s admit it, I’m trying to sell you a database. But when I see a string like that, the resounding chime of “Primary Key” echoes in my mind.
Using this key, we can track the world records for each scramble. And there is a clear winner in the event of a conflict. The winner is the record with the lowest time. This is a simple rule that we can implement programmatically in Dolt.
Defining the Data Model that Conflicts#
Let’s say we have a table that tracks the world records for speed cubing. We have a table called world_records with the following schema:
CREATE TABLE world_records (
scramble_id VARCHAR(255) PRIMARY KEY,
record_time_seconds FLOAT,
record_holder VARCHAR(255),
);
Programmatic resolution of conflicts in this model is straightforward. In Dolt terms, conflicts arise when two edits are made to the same row in a table. The row is identified by its primary key. The scramble_id is the primary key, so if two branches have conflicting changes to the same scramble_id, we can programmatically determine the winner by comparing the record_time_seconds values. The record with the lowest time is the winner.
This is a critical piece to understand as you are building your application, especially if you will rely on Dolt branching in an automated way. Your application must have a programmatic way to resolve a conflict. In this case, the winner is the record with the lowest time. Your application will likely be much more complex than this, but the principle is the same. It is possible to design a data model that never conflicts, which is another approach. However, using conflicts as a feature is a powerful way to allow for concurrent updates to the same data, and it can be a great way to build a collaborative application.
A third approach is to resolve automatically by choosing “ours” or “theirs”. This can make sense for some applications, and we provide a stored procedure to do this. This blog is really for when that’s not an option.
Primitives to Work With#
Before we dive into an example, let’s review the primitives that Dolt provides for working with conflicts. When a merge conflict occurs, there will be an error stating as much. From that point forward, you have two primary primitives to work with: the dolt_conflicts system table and the dolt_conflicts_{$table} system tables.
dolt_conflicts#
> SHOW CREATE TABLE dolt_conflicts;
+----------------+---------------------------------------------+
| Table | Create Table |
+----------------+---------------------------------------------+
| dolt_conflicts | CREATE TABLE `dolt_conflicts` ( |
| | `table` text NOT NULL, |
| | `num_conflicts` bigint unsigned NOT NULL, |
| | PRIMARY KEY (`table`) |
| | ) |
+----------------+---------------------------------------------+
When your conflict occurs, the first thing you’ll need to know is which tables have conflicts. The dolt_conflicts table will tell you that. It has two columns: table, which is the name of the table that has conflicts, and num_conflicts, which is the number of conflicting rows in that table.
dolt_conflicts_{$table}#
For each row in the dolt_conflicts table, there will be a corresponding dolt_conflicts_{$table} table that contains the details of the conflicts for that particular table. The schema of this table is determined by the schema of the original table. Each column in the original table is mapped to three columns, prefixed by our_, their_, and base_. You can read about it in our documentation. The critical thing to know is that for each row that conflicts, there is enough information in the dolt_conflicts_{$table} table to allow you to programmatically resolve the conflict. It is effectively the three-way merge information for each conflicting row. We’ll cover this in more detail in the example.
Finally, and maybe the most important thing to know, is that users must delete rows from the dolt_conflicts_{$table} table. This is how you indicate to Dolt that a given conflict is resolved. It is not possible to complete your merge until all rows have been deleted from the dolt_conflicts_{$table} table.
Doing It in Dolt#
Let’s go through the mechanics with an example.
If you don’t have Dolt, you can install it. Start a shell:
$ mkdir mydb; cd mydb
$ dolt init
$ dolt sql
# Welcome to the DoltSQL shell.
# Statements must be terminated with ';'.
# "exit" or "quit" (or Ctrl-D) to exit. "\help" for help.
mydb/main>
Create the world_records table with the appropriate schema. We’ll add a baseline record for the scramble we’re interested in, but we’ll set the time to a very high value and the record holder to “Unsolved”. This record must exist as the merge base for the conflict resolution to work properly.
mydb/main> CREATE TABLE world_records (
scramble_id VARCHAR(255) PRIMARY KEY,
record_time_seconds FLOAT,
record_holder VARCHAR(255)
);
mydb/main*> INSERT INTO world_records VALUES (
'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2',
9999.99,
'Unsolved'
);
mydb/main*> CALL DOLT_COMMIT('-Am',"Initialize table with unsolved scramble baseline");
Now we simulate a divergence. We create two branches: “neil-solve” and “teodor-solve”. On each branch, we update the same row with different values. This will create a conflict when we attempt to merge both branches back into main.
mydb/main> CALL DOLT_BRANCH('neil-solve');
mydb/main> CALL DOLT_BRANCH('teodor-solve');
Check out Neil’s branch. I executed this puzzle in a little over 4 minutes. I will submit my time to the world record committee:
mydb/main> CALL DOLT_CHECKOUT('neil-solve');
mydb/neil-solve> UPDATE world_records SET
record_time_seconds = 246.8,
record_holder = 'Neil Macneale'
WHERE scramble_id = 'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mydb/neil-solve*> CALL DOLT_COMMIT('-am',"Neil finishes the cube in 4 minutes");
Now, check out Teodor’s branch. He executes the same puzzle in a little under 3 seconds. He submits his time to the world record committee:
mydb/neil-solve> CALL DOLT_CHECKOUT('teodor-solve');
mydb/teodor-solve> UPDATE world_records SET
record_time_seconds = 2.76,
record_holder = 'Teodor Zajder'
WHERE scramble_id = 'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mydb/teodor-solve*> CALL DOLT_COMMIT('-am',"Teodor finishes the cube in 2.76 seconds");
The main branch has the baseline record of 9999.99 seconds. The neil-solve branch has a record of 246.8 seconds, and the teodor-solve branch has a record of 2.76 seconds.
You can merge either branch into main without conflict, but when you attempt to merge the second branch, you will get a conflict because both branches updated the same row in different ways.
mydb/teodor> CALL DOLT_CHECKOUT('main');
mydb/main> CALL DOLT_MERGE('--no-ff', 'neil-solve');
mydb/main> SELECT record_holder,record_time_seconds FROM world_records
WHERE scramble_id = 'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2';
+---------------+---------------------+
| record_holder | record_time_seconds |
+---------------+---------------------+
| Neil Macneale | 246.8 |
+---------------+---------------------+
1 row in set (0.00 sec)
There it is, my moment in the sun. I’m on the board. Now let’s merge Teodor’s branch.
mydb/main> CALL DOLT_MERGE('--no-ff', 'teodor-solve');
+------+--------------+-----------+-----------------+
| hash | fast_forward | conflicts | message |
+------+--------------+-----------+-----------------+
| | 0 | 1 | conflicts found |
+------+--------------+-----------+-----------------+
1 row in set, 1 warning (0.00 sec)
Warning (Code 1105): merge has unresolved conflicts or constraint violations
Merge conflict detected, @autocommit transaction rolled back. @autocommit must be disabled so that merge conflicts can be resolved using the dolt_conflicts and dolt_schema_conflicts tables before manually committing the transaction. Alternatively, to commit transactions with merge conflicts, set @@dolt_allow_commit_conflicts = 1
The @autocommit error is a common stumbling block for users new to conflict resolution in Dolt. The error indicates that the merge has unresolved conflicts and that the transaction has been rolled back. This means that the merge has not even started.
To move forward, you need to disable @autocommit and then resolve the conflict using the dolt_conflicts and dolt_conflicts_world_records tables. Once all conflicts have been resolved, you can manually commit the transaction.
mydb/main> SET autocommit = 0;
mydb/main> CALL DOLT_MERGE('--no-ff', 'teodor-solve');
+------+--------------+-----------+-----------------+
| | 0 | 1 | conflicts found |
+------+--------------+-----------+-----------------+
1 row in set, 1 warning (0.00 sec)
Warning (Code 1105): merge has unresolved conflicts or constraint violations
mydb/main*>
Now we can see the conflict in the dolt_conflicts table, and we can see the details of the conflict in the dolt_conflicts_world_records table.
mydb/main*> \status
On branch main
You have unmerged tables.
(fix conflicts and run "dolt commit")
(use "dolt merge --abort" to abort the merge)
Unmerged paths:
(use "dolt add <table>..." to mark resolution)
both modified: world_records
mydb/main*> select * from dolt_conflicts;
+---------------+---------------+
| table | num_conflicts |
+---------------+---------------+
| world_records | 1 |
+---------------+---------------+
1 row in set (0.00 sec)
mydb/main*> select * from dolt_conflicts_world_records;
+----------------------------------+--------------------------------------------+--------------------------+--------------------+--------------------------------------------+-------------------------+-------------------+---------------+--------------------------------------------+---------------------------+---------------------+-----------------+------------------------+
| from_root_ish | base_scramble_id | base_record_time_seconds | base_record_holder | our_scramble_id | our_record_time_seconds | our_record_holder | our_diff_type | their_scramble_id | their_record_time_seconds | their_record_holder | their_diff_type | dolt_conflict_id |
+----------------------------------+--------------------------------------------+--------------------------+--------------------+--------------------------------------------+-------------------------+-------------------+---------------+--------------------------------------------+---------------------------+---------------------+-----------------+------------------------+
| aobtf9gna5a1uddosqjru3dhnujuf61i | L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2 | 9999.99 | Unsolved | L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2 | 246.8 | Neil Macneale | modified | L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2 | 2.76 | Teodor Zajder | modified | EvVtyTUR+5KTv3Kta2MWYw |
+----------------------------------+--------------------------------------------+--------------------------+--------------------+--------------------------------------------+-------------------------+-------------------+---------------+--------------------------------------------+---------------------------+---------------------+-----------------+------------------------+
1 row in set (0.01 sec)
That’s a really wide table. Let’s look at it in record format:
mydb/main*> select * from dolt_conflicts_world_records\G
*************************** 1. row ***************************
from_root_ish: aobtf9gna5a1uddosqjru3dhnujuf61i
base_scramble_id: L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2
base_record_time_seconds: 9999.99
base_record_holder: Unsolved
our_scramble_id: L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2
our_record_time_seconds: 246.8
our_record_holder: Neil Macneale
our_diff_type: modified
their_scramble_id: L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2
their_record_time_seconds: 2.76
their_record_holder: Teodor Zajder
their_diff_type: modified
dolt_conflict_id: EvVtyTUR+5KTv3Kta2MWYw
1 row in set (0.00 sec)
Remember above when I said that the dolt_conflicts_{$table} table contains the three-way merge information for each conflicting row? This is what I meant. For each column in the original table, there are three columns in the dolt_conflicts_{$table} table: one for “ours,” one for “theirs,” and one for “base.” This allows you to programmatically determine the winner of the conflict by comparing the values in the “ours” and “theirs” columns.
If we look at each piece individually, it might be easier to understand:
mydb/main*> select base_record_time_seconds, base_record_holder from dolt_conflicts_world_records where base_scramble_id = 'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2';
+--------------------------+--------------------+
| base_record_time_seconds | base_record_holder |
+--------------------------+--------------------+
| 9999.99 | Unsolved |
+--------------------------+--------------------+
1 row in set (0.00 sec)
mydb/main*> select our_record_time_seconds, our_record_holder from dolt_conflicts_world_records where our_scramble_id = 'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2';
+-------------------------+-------------------+
| our_record_time_seconds | our_record_holder |
+-------------------------+-------------------+
| 246.8 | Neil Macneale |
+-------------------------+-------------------+
1 row in set (0.00 sec)
mydb/main*> select their_record_time_seconds, their_record_holder from dolt_conflicts_world_records where their_scramble_id = 'L B R2 B\' R2 U2 F D R2 U R2 F2 D2 R U B L2';
+---------------------------+---------------------+
| their_record_time_seconds | their_record_holder |
+---------------------------+---------------------+
| 2.76 | Teodor Zajder |
+---------------------------+---------------------+
1 row in set (0.00 sec)
The three-way merge information allows us to programmatically determine the winner of the conflict. In this case, we can see that the “theirs” record has a lower time than the “ours” record, so we can determine that the winner of the conflict is Teodor Zajder. I concede, the kid can turn a cube faster than I can. I’m shocked.
Resolving the Conflict#
Resolving a data conflict involves three steps:
- Correct the data
- Delete the row from the
dolt_conflicts_{$table}table - Add and commit
Correcting the data is performed by modifying the data in the table itself. In this case, we want to update the world_records table to reflect the new world record. We can do this with an UPDATE statement that sets the record_time_seconds and record_holder columns to the values from the “theirs” columns in the dolt_conflicts_world_records table.
mydb/main*> UPDATE world_records wr
JOIN dolt_conflicts_world_records dcr ON wr.scramble_id = dcr.our_scramble_id
SET wr.record_time_seconds = IF(dcr.their_record_time_seconds < dcr.our_record_time_seconds,
dcr.their_record_time_seconds,
dcr.our_record_time_seconds),
wr.record_holder = IF(dcr.their_record_time_seconds < dcr.our_record_time_seconds,
dcr.their_record_holder,
dcr.our_record_holder);
We can verify that the data has been corrected by looking at the diff:
mydb/main*> mydb/main*> \diff
diff --dolt a/world_records b/world_records
--- a/world_records
+++ b/world_records
+---+--------------------------------------------+---------------------+---------------+
| | scramble_id | record_time_seconds | record_holder |
+---+--------------------------------------------+---------------------+---------------+
| < | L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2 | 246.8 | Neil Macneale |
| > | L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2 | 2.76 | Teodor Zajder |
+---+--------------------------------------------+---------------------+---------------+
1 row in set (0.00 sec)
The way you resolve a conflict is by deleting the row from the dolt_conflicts_{$table} table. This is how you indicate to Dolt that a given conflict is resolved. It is not possible to complete your merge until all rows have been deleted from the dolt_conflicts_{$table} table. Even though we have corrected the data, the conflict record still exists, so we need to delete it.
mydb/main*> select base_scramble_id,dolt_conflict_id from dolt_conflicts_world_records;
+--------------------------------------------+------------------------+
| base_scramble_id | dolt_conflict_id |
+--------------------------------------------+------------------------+
| L B R2 B' R2 U2 F D R2 U R2 F2 D2 R U B L2 | EvVtyTUR+5KTv3Kta2MWYw |
+--------------------------------------------+------------------------+
1 row in set (0.00 sec)
mydb/main*> delete from dolt_conflicts_world_records where dolt_conflict_id = 'EvVtyTUR+5KTv3Kta2MWYw';
Query OK, 1 row affected (0.00 sec)
Now that the conflict has been resolved, we can commit the merge.
mydb/main*> CALL DOLT_COMMIT('-am',"Merge Teodor's win as the new world record");
+----------------------------------+
| hash |
+----------------------------------+
| abkc66ifi3mm19k87s2hmqo7r1m9oqqq |
+----------------------------------+
1 row in set (0.01 sec)
The Programmatic Part#
The example above was meant to illustrate the mechanics of how to resolve a conflict in Dolt. In a real-world scenario, you will have some application process that automatically merges branches and resolves conflicts. In that application, you would loop over the rows in the dolt_conflicts table to determine which tables have conflicts, and then for each table, you would query the corresponding dolt_conflicts_{$table} table to get the details of the conflicts. You would then apply your application-specific logic to determine the winner of each conflict, correct the data in the original table, and delete the conflict record from the dolt_conflicts_{$table} table. Once all conflicts have been resolved, you would commit the merge.
Regardless of your application stack, the steps are the same. It can all be automated as long as you have a well-defined way to resolve a conflict.
Come to our Discord and let us know what you build with this new feature, or if you have any questions or feedback!