docs / reference
Conflict resolution
How botdb merges concurrent changes from devices that edited offline, for rows, columns, deletes and schema.
Every device writes to its own local copy and syncs later, so two devices can change the same data without seeing each other’s changes. botdb merges them with CRDT rules: every replica that has seen the same set of changes ends up with the same data, regardless of the order they arrived in.
Changes are logical
Each local transaction becomes one batch of logical changes:
| Change | Meaning |
|---|---|
insert |
A row exists (with its causal length). |
delete |
A row is deleted (with its causal length). |
set |
One cell’s new value. |
| schema changes | Create, drop or rename tables and columns; create or drop indexes and views. |
Changes are captured by triggers on each synced table, stamped with the transaction’s hybrid logical clock, and appended to the local log. Tables and columns are referenced by stable ids, and rows by their primary key encoded as a JSON array. Each batch is identified by its replica id (site) and a sequence number that increases by one per batch on that replica.
Columns: last writer wins
Each cell remembers the hybrid logical clock (HLC) timestamp and the replica id of its last write. When two devices set the same cell, the higher (timestamp, replica id) wins. Different columns of the same row merge independently.
An HLC packs 48 bits of wall-clock milliseconds and a 16-bit counter. It never goes backwards on a device, and a device that receives a newer timestamp moves its clock past it, so causally later writes always win. Ties between devices are broken by replica id.
Rows: causal length
Each row has a causal length (cl): odd means the row exists, even means it’s deleted. Inserting a row gives it cl = 1, deleting it moves it to the next even number, inserting it again to the next odd one. Replicas keep the highest cl they’ve seen.
Cell values are tagged with the row’s cl. Values written at a lower cl than the row’s current one are ignored, and last-writer-wins only compares values written at the same cl.
The result:
- A delete beats concurrent updates. Both devices had the row at
cl = 1; the delete moves it tocl = 2, and the update (still atcl = 1) is ignored. - Re-inserting resurrects the row. The re-insert moves it to
cl = 3, with the values written by that insert.
Examples
| Device A | Device B (concurrently) | Result after sync |
|---|---|---|
SET rating = 4 |
SET title = 'Dune 2' |
Both changes kept. |
SET rating = 4 |
SET rating = 5, a moment later |
rating = 5 (later timestamp). |
DELETE the row |
SET rating = 5 |
Row deleted. |
DELETE the row, then re-insert |
SET rating = 5 before the delete |
Row exists with the re-inserted values. |
Insert imdb_id = 'tt15239678' |
Insert imdb_id = 'tt15239678' |
One row; each column keeps the latest write. |
| Insert with a random id | Insert the same movie, random id | Two rows. Use a natural key to avoid this. |
Schema
Schema changes merge the same way. Tables and columns have stable ids; their names and whether they exist are last-writer-wins registers ordered by (timestamp, replica id).
- Table ids are derived from the table name and its primary-key definition, and column ids from the table, column name and type. Two devices that independently create a table with the same name and primary key end up with one merged table.
- Otherwise, when two tables (or two columns of a table) end up with the same name, the one named most recently keeps the name and the other gets a
_<id prefix>suffix. Primary-key columns always keep their names. Every device resolves this the same way. - Concurrent renames converge on the latest one.
- Dropping a table drops its columns, so re-creating it later only brings back the columns of the new definition.
- A change that references a table or column a device hasn’t seen yet is held back and applied once the schema change arrives.
Read-only members
A member who joined with a read-only invite can’t write locally: writes fail with read_only. Relays also refuse their pushes, since they lack the write key.
Clocks
Last-writer-wins depends on wall clocks being roughly right. A device whose clock runs ahead wins concurrent writes to the same cell until real time catches up. Relays also reject requests more than 5 minutes off their clock, so keep NTP enabled.
Sync bookkeeping
Each replica tracks, per replica id, the highest contiguous sequence number it has applied. Comparing these vectors tells a replica which batches a remote is missing. Batches that arrive out of order (common with cloud folders) are kept and applied; the contiguous mark only advances once the gap is filled, so a withheld batch is detectable. Applying a batch twice has no effect.