AutoMerge
This documentation is also tests for the code, the examples below show the literal output of these statements from Postgres.
The automerge
extension is the first (of hopefully several) CRDT
libraries provided by the pg_crdt
package. The Automerge
Library provides a json-like document
format which allows concurrent changes on different devices to be
merged automatically without requiring any central server. Changes
from different servers and sources can be applied to documents to
bring them up to date, or whole documents can be "merged" to
combine them without conflicts.
Some setup to make sure the extension is installed.
create extension if not exists automerge;
set search_path to public,automerge;
Let's make a test table to contain our automerge documents.
create table if not exists test (
id bigserial,
doc autodoc not null default '{}'
);
Casting to/from jsonb
Automerge centers around a document object called
automerge.autodoc
. An autodoc is a json object "like" container
for (mostly) json like types. Documents can be created by casting
an initializing a json/jsonb object to autodoc
:
insert into test (doc) values ('{"foo":1}') returning id \gset
select pg_typeof(doc) from test where id = :id;
┌───────────┐
│ pg_typeof │
├───────────┤
│ autodoc │
└───────────┘
(1 row)
An autodoc
instance looks like a bytea
type, which internally
encodes the state of the document. This casting operation supports
all the Postgres jsonb types. jsonb doesn't have a distinction
between integers and floats, but autodoc automatically detects them
and creates the right autodoc value.
You can cast back to jsonb
as well:
select doc::jsonb from test where id = :id;
┌────────────┐
│ doc │
├────────────┤
│ {"foo": 1} │
└────────────┘
(1 row)
Note that casting to jsonb is a potentially lossy operation, since
autodoc support more types of values than jsonb does, such as text
objects, counters and timestamps. Text objects are converted to
json strings, Counters are converted to integers, and timestamps to
UTC strings. There is no JSON input representation for text,
counters or timestamps, you must use put_text
, put_counter
or
put_timestamp
(see below) to set those types explicity on
documents.
Getting scalar values
Individual scalar values can be retrieved from an autodoc with
_get
functions and set with _put
functions. These functions
take a path syntax that can traverse the document and its
sub-objects/arrays. An object is traversed with the .
operator
and array items are indexed with the []
operator:
Integers
Autodoc integers are mapped to postgres 64-bit signed integers
(bigint
). They can be initialized from jsonb using json
integers, or put directly into an existing autdoc.
insert into test (doc) values ('{"foo":1,"far":{"bar":[1,2,3]}}') returning id \gset
select get_int(doc, '.foo') from test where id = :id;
┌─────────┐
│ get_int │
├─────────┤
│ 1 │
└─────────┘
(1 row)
select get_int(doc, '.far.bar[1]') from test where id = :id;
┌─────────┐
│ get_int │
├─────────┤
│ 2 │
└─────────┘
(1 row)
update test set doc = put_int(doc, '.fiz', 2) where id = :id returning doc::jsonb;
┌─────────────────────────────────────────────────┐
│ doc │
├─────────────────────────────────────────────────┤
│ {"far": {"bar": [1, 2, 3]}, "fiz": 2, "foo": 1} │
└─────────────────────────────────────────────────┘
(1 row)
put_int
(and other put_
functions) take an optional boolean
argument insert
. If true, the value will be inserted after the
provided position into an array instead of overwriting it. If the
value is not an array, the insert
argument is ignored:
update test set doc = put_int(doc, '.far.bar[1]', 3, false) where id = :id returning doc::jsonb;
┌─────────────────────────────────────────────────┐
│ doc │
├─────────────────────────────────────────────────┤
│ {"far": {"bar": [1, 3, 3]}, "fiz": 2, "foo": 1} │
└─────────────────────────────────────────────────┘
(1 row)
update test set doc = put_int(doc, '.far.bar[1]', 3, true) where id = :id returning doc::jsonb;
┌────────────────────────────────────────────────────┐
│ doc │
├────────────────────────────────────────────────────┤
│ {"far": {"bar": [1, 3, 3, 3]}, "fiz": 2, "foo": 1} │
└────────────────────────────────────────────────────┘
(1 row)
Strings
Automerge strings map to Postgres text
type. They can be
initialized from jsonb strings or using the same get/put API as
integers:
insert into test (doc) values ('{"foo":"fiz","bar":["one","two","three"]}') returning id \gset
select get_str(doc, '.foo') from test where id = :id;
┌─────────┐
│ get_str │
├─────────┤
│ fiz │
└─────────┘
(1 row)
select get_str(doc, '.bar[1]') from test where id = :id;
┌─────────┐
│ get_str │
├─────────┤
│ two │
└─────────┘
(1 row)
update test set doc = put_str(doc, '.bing', 'bang') where id = :id returning doc::jsonb;
┌────────────────────────────────────────────────────────────────┐
│ doc │
├────────────────────────────────────────────────────────────────┤
│ {"bar": ["one", "two", "three"], "foo": "fiz", "bing": "bang"} │
└────────────────────────────────────────────────────────────────┘
(1 row)
update test set doc = put_str(doc, '.bar[1]', 'three', false) where id = :id returning doc::jsonb;
┌──────────────────────────────────────────────────────────────────┐
│ doc │
├──────────────────────────────────────────────────────────────────┤
│ {"bar": ["one", "three", "three"], "foo": "fiz", "bing": "bang"} │
└──────────────────────────────────────────────────────────────────┘
(1 row)
update test set doc = put_str(doc, '.bar[1]', 'three', true) where id = :id returning doc::jsonb;
┌───────────────────────────────────────────────────────────────────────────┐
│ doc │
├───────────────────────────────────────────────────────────────────────────┤
│ {"bar": ["one", "three", "three", "three"], "foo": "fiz", "bing": "bang"} │
└───────────────────────────────────────────────────────────────────────────┘
(1 row)
Doubles
Doubles map to postgres double precision
type, and can be
initialized from jsonb decimal numbers:
insert into test (doc) values ('{"pi":3.14159,"foo":{"bar":[1.1,2.2,3.3]}}') returning id \gset
select get_double(doc, '.pi') from test where id = :id;
┌────────────┐
│ get_double │
├────────────┤
│ 3.14159 │
└────────────┘
(1 row)
select get_double(doc, '.foo.bar[1]') from test where id = :id;
┌────────────┐
│ get_double │
├────────────┤
│ 2.2 │
└────────────┘
(1 row)
update test set doc = put_double(doc, '.e', 2.71828) where id = :id returning doc::jsonb;
┌────────────────────────────────────────────────────────────────┐
│ doc │
├────────────────────────────────────────────────────────────────┤
│ {"e": 2.71828, "pi": 3.14159, "foo": {"bar": [1.1, 2.2, 3.3]}} │
└────────────────────────────────────────────────────────────────┘
(1 row)
update test set doc = put_double(doc, '.foo.bar[1]', 3.3, false) where id = :id returning doc::jsonb;
┌────────────────────────────────────────────────────────────────┐
│ doc │
├────────────────────────────────────────────────────────────────┤
│ {"e": 2.71828, "pi": 3.14159, "foo": {"bar": [1.1, 3.3, 3.3]}} │
└────────────────────────────────────────────────────────────────┘
(1 row)
update test set doc = put_double(doc, '.foo.bar[1]', 3.3, true) where id = :id returning doc::jsonb;
┌─────────────────────────────────────────────────────────────────────┐
│ doc │
├─────────────────────────────────────────────────────────────────────┤
│ {"e": 2.71828, "pi": 3.14159, "foo": {"bar": [1.1, 3.3, 3.3, 3.3]}} │
└─────────────────────────────────────────────────────────────────────┘
(1 row)
Bools
Bools map to the Postgres bool
type:
insert into test (doc) values ('{"foo":true,"bar":[true,false,true]}') returning id \gset
select get_bool(doc, '.foo') from test where id = :id;
┌──────────┐
│ get_bool │
├──────────┤
│ t │
└──────────┘
(1 row)
select get_bool(doc, '.bar[1]') from test where id = :id;
┌──────────┐
│ get_bool │
├──────────┤
│ f │
└──────────┘
(1 row)
update test set doc = put_bool(doc, '.bar[1]', false) where id = :id returning doc::jsonb;
┌──────────────────────────────────────────────────┐
│ doc │
├──────────────────────────────────────────────────┤
│ {"bar": [true, false, false, true], "foo": true} │
└──────────────────────────────────────────────────┘
(1 row)
update test set doc = put_bool(doc, '.bar[1]', true) where id = :id returning doc::jsonb;
┌────────────────────────────────────────────────────────┐
│ doc │
├────────────────────────────────────────────────────────┤
│ {"bar": [true, true, false, false, true], "foo": true} │
└────────────────────────────────────────────────────────┘
(1 row)
Counters
Automerge counters are like integers, but can be concurrently updated and merged without losing count.
NOTE: Counters have no jsonb input representation, on output they are represented as JSON integer.
insert into test (doc) values (put_counter('{}', '.bar', 1)) returning id \gset
select get_counter(doc, '.bar') from test where id = :id;
┌─────────────┐
│ get_counter │
├─────────────┤
│ 1 │
└─────────────┘
(1 row)
update test set doc = inc_counter(doc, '.bar') where id = :id returning doc::jsonb;
┌────────────┐
│ doc │
├────────────┤
│ {"bar": 2} │
└────────────┘
(1 row)
update test set doc = inc_counter(doc, '.bar', 2) where id = :id returning doc::jsonb;
┌────────────┐
│ doc │
├────────────┤
│ {"bar": 4} │
└────────────┘
(1 row)
update test set doc = inc_counter(doc, '.bar', -2) where id = :id returning doc::jsonb;
┌────────────┐
│ doc │
├────────────┤
│ {"bar": 2} │
└────────────┘
(1 row)
Text
Automerge Text objects are like strings but have support for changing ("splicing") text in and out efficiently. Compared to strings which are updated atomically, text values are internally represented by an array of characters and can be modified in place.
NOTE: Text have no jsonb input representation, on output they are represented as JSON string.
insert into test (doc) values (put_text('{}', '.foo', 'hello postgres')) returning id as text_id \gset
select get_text(doc, '.foo') from test where id = :text_id;
┌────────────────┐
│ get_text │
├────────────────┤
│ hello postgres │
└────────────────┘
(1 row)
update test set doc = splice_text(doc, '.foo', 6, 14, 'world') where id = :text_id returning doc::jsonb;
┌────────────────────────┐
│ doc │
├────────────────────────┤
│ {"foo": "hello world"} │
└────────────────────────┘
(1 row)
Marks
Marks are objects that span a region of a text object decorating that region with information such as "bold" or "italic".
update test set doc = create_mark(doc, '.foo', 1, 2, 'bold', true) where id = :text_id;
update test set doc = create_mark(doc, '.foo', 3, 10, 'font_size', 42) where id = :text_id;
select * from get_marks((select doc from test where id = :text_id), '.foo');
┌───────────┬───────────┬─────────┬──────┐
│ name │ start_pos │ end_pos │ val │
├───────────┼───────────┼─────────┼──────┤
│ bold │ 1 │ 2 │ true │
│ font_size │ 3 │ 10 │ 42 │
└───────────┴───────────┴─────────┴──────┘
(2 rows)
Actor Ids
Automerge supports a notion of "Actor Ids" that identify the actors
making concurrent changes to documents. This UUID data can be get
and set with get_actor_id(autodoc)
and set_actor_id(autodoc,
uuid)
. Postgres stores the last set actor id for a document which
gets preserved for future changes. If no actor id is set,
automerge generates a random one:
update test set doc = set_actor_id(doc, '97131c66344c48e8b93249aabff6b2f2') where id = :text_id;
select get_actor_id(doc) from test where id = :text_id;
┌────────────────────────────────────────────────────────────────────┐
│ get_actor_id │
├────────────────────────────────────────────────────────────────────┤
│ \x3937313331633636333434633438653862393332343961616266663662326632 │
└────────────────────────────────────────────────────────────────────┘
(1 row)
Merging documents
Documents can be merged together without conflict. The second argument document is merged into the first:
insert into test (doc) values (
merge((select doc from test where id = :id), (select doc from test where id = :text_id))
) returning id as merge_id \gset
Getting Changes
All changes can be retrieved with the get_changes()
function:
Get a change hash, message and actor_id:
select pg_typeof(get_change_hash(c)),
get_change_message(c),
pg_typeof(get_actor_id(c))
from get_changes((select doc from test where id = :merge_id)) c;
┌───────────┬────────────────────┬───────────┐
│ pg_typeof │ get_change_message │ pg_typeof │
├───────────┼────────────────────┼───────────┤
│ bytea │ put_counter │ bytea │
│ bytea │ │ bytea │
│ bytea │ │ bytea │
│ bytea │ │ bytea │
│ bytea │ │ bytea │
│ bytea │ │ bytea │
│ bytea │ │ bytea │
│ bytea │ │ bytea │
└───────────┴────────────────────┴───────────┘
(8 rows)
Apply a change from one doc to another:
select * from get_changes('{"foo":{"bar":1}}') change limit 1 \gset
select apply('{"baz":true}', :'change')::jsonb;
┌──────────────────────────────────┐
│ apply │
├──────────────────────────────────┤
│ {"baz": true, "foo": {"bar": 1}} │
└──────────────────────────────────┘
(1 row)
The final state of the test table:
select doc::jsonb from test order by id;
┌───────────────────────────────────────────────────────────────────────────┐
│ doc │
├───────────────────────────────────────────────────────────────────────────┤
│ {"foo": 1} │
│ {"far": {"bar": [1, 3, 3, 3]}, "fiz": 2, "foo": 1} │
│ {"bar": ["one", "three", "three", "three"], "foo": "fiz", "bing": "bang"} │
│ {"e": 2.71828, "pi": 3.14159, "foo": {"bar": [1.1, 3.3, 3.3, 3.3]}} │
│ {"bar": [true, true, false, false, true], "foo": true} │
│ {"bar": 2} │
│ {"foo": "hello world"} │
│ {"bar": 2, "foo": "hello world"} │
└───────────────────────────────────────────────────────────────────────────┘
(8 rows)