You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2019/04/29 19:28:53 UTC

[couchdb] branch prototype/rfc-001-revision-metadata-model updated: More tests

This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch prototype/rfc-001-revision-metadata-model
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/prototype/rfc-001-revision-metadata-model by this push:
     new ca63700  More tests
ca63700 is described below

commit ca63700f98ba59459bac3b3283048614ccfc2d5d
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Mon Apr 29 14:28:41 2019 -0500

    More tests
---
 src/fabric/src/fabric2_db.erl              |  91 ++++---
 src/fabric/src/fabric2_fdb.erl             |   2 +-
 src/fabric/src/fabric2_util.erl            |   7 +-
 src/fabric/test/fabric2_db_misc_tests.erl  |  34 ++-
 src/fabric/test/fabric2_doc_crud_tests.erl | 374 +++++++++++++++++++++++++++++
 5 files changed, 469 insertions(+), 39 deletions(-)

diff --git a/src/fabric/src/fabric2_db.erl b/src/fabric/src/fabric2_db.erl
index eaa2faf..ed43fea 100644
--- a/src/fabric/src/fabric2_db.erl
+++ b/src/fabric/src/fabric2_db.erl
@@ -306,12 +306,13 @@ get_security(#{security_doc := SecurityDoc}) ->
 
 
 get_update_seq(#{} = Db) ->
-    case fabric2_fdb:get_changes(Db, [{limit, 1}, {reverse, true}]) of
-        [] ->
-            fabric2_util:to_hex(<<0:80>>);
-        [{Seq, _}] ->
-            Seq
-    end.
+    fabric2_fdb:transactional(Db, fun(TxDb) ->
+        Opts = [{limit, 1}, {reverse, true}],
+        case fabric2_fdb:get_changes(TxDb, Opts) of
+            [] -> fabric2_util:to_hex(<<0:80>>);
+            [{Seq, _}] -> Seq
+        end
+    end).
 
 
 get_user_ctx(#{user_ctx := UserCtx}) ->
@@ -406,9 +407,12 @@ open_doc(#{} = Db, DocId, _Options) ->
 open_doc_revs(Db, DocId, Revs, Options) ->
     Latest = lists:member(latest, Options),
     fabric2_fdb:transactional(Db, fun(TxDb) ->
-        #full_doc_info{
-            rev_tree = RevTree
-        } = fabric2_db:get_full_doc_info(TxDb, DocId),
+        AllRevInfos = fabric2_fdb:get_all_revs(Db, DocId),
+        RevTree = lists:foldl(fun(RI, TreeAcc) ->
+            RIPath = fabric2_util:revinfo_to_path(RI),
+            {Merged, _} = couch_key_tree:merge(TreeAcc, RIPath),
+            Merged
+        end, [], AllRevInfos),
         {Found, Missing} = case Revs of
             all ->
                 {couch_key_tree:get_all_leafs(RevTree), []};
@@ -439,9 +443,18 @@ update_doc(Db, Doc) ->
 
 
 update_doc(Db, Doc, Options) ->
-    fabric2_fdb:transactional(Db, fun(TxDb) ->
-        update_doc_int(TxDb, Doc, Options)
-    end).
+    case update_docs(Db, [Doc], Options) of
+        {ok, [{ok, NewRev}]} ->
+            {ok, NewRev};
+        {ok, [{{_Id, _Rev}, Error}]} ->
+            throw(Error);
+        {error, [Error]} ->
+            throw(Error);
+        {ok, []} ->
+            % replication success
+            {Pos, [RevId | _]} = Doc#doc.revs,
+            {ok, {Pos, RevId}}
+    end.
 
 
 update_docs(Db, Docs) ->
@@ -449,7 +462,7 @@ update_docs(Db, Docs) ->
 
 
 update_docs(Db, Docs, Options) ->
-    {Resps, Status} = lists:mapfoldl(fun(Doc, Acc) ->
+    {Resps0, Status} = lists:mapfoldl(fun(Doc, Acc) ->
         fabric2_fdb:transactional(Db, fun(TxDb) ->
             case update_doc_int(TxDb, Doc, Options) of
                 {ok, _} = Resp ->
@@ -459,7 +472,12 @@ update_docs(Db, Docs, Options) ->
             end
         end)
     end, ok, Docs),
-    {Status, Resps}.
+    Resps1 = case lists:member(replicated_changes, Options) of
+        true -> [R || R <- Resps0, R /= {ok, []}];
+        false -> Resps0
+    end,
+    %io:format(standard_error, "~nRESPS: ~p :: ~p~n~n", [Resps0, Resps1]),
+    {Status, Resps1}.
 
 
 fold_docs(Db, UserFun, UserAcc) ->
@@ -677,11 +695,8 @@ update_doc_interactive(Db, Doc0, _Options) ->
     Doc1 = case Winner of
         #{deleted := true} when not Doc0#doc.deleted ->
             {WinnerRevPos, WinnerRev} = maps:get(rev_id, Winner),
-            Doc0#doc{revs = {WinnerRevPos, [WinnerRev]}};
-        #{deleted := true} when Doc0#doc.deleted ->
-            % We disable extending deleted revisions with
-            % new deletions during interactive updates
-            ?RETURN({error, conflict});
+            WinnerRevPath = maps:get(rev_path, Winner),
+            Doc0#doc{revs = {WinnerRevPos, [WinnerRev | WinnerRevPath]}};
         _ ->
             Doc0
     end,
@@ -722,7 +737,11 @@ update_doc_interactive(Db, Doc0, _Options) ->
         [W, NW] -> {W, NW}
     end,
 
-    NewWinner = NewWinner0#{branch_count := maps:get(branch_count, Winner)},
+    BranchCount = case Winner of
+        not_found -> 1;
+        #{branch_count := BC} -> BC
+    end,
+    NewWinner = NewWinner0#{branch_count := BranchCount},
     ToUpdate = if NonWinner == not_found -> []; true -> [NonWinner] end,
     ToRemove = if Target == not_found -> []; true -> [Target] end,
 
@@ -745,7 +764,7 @@ update_doc_replicated(Db, Doc0, _Options) ->
         revs = {RevPos, [Rev | RevPath]}
     } = Doc0,
 
-    DocRevInfo = #{
+    DocRevInfo0 = #{
         winner => undefined,
         deleted => Deleted,
         rev_id => {RevPos, Rev},
@@ -762,7 +781,7 @@ update_doc_replicated(Db, Doc0, _Options) ->
         Merged
     end, [], AllRevInfos),
 
-    DocRevPath = fabric2_util:revinfo_to_path(DocRevInfo),
+    DocRevPath = fabric2_util:revinfo_to_path(DocRevInfo0),
     {NewTree, Status} = couch_key_tree:merge(RevTree, DocRevPath),
     if Status /= internal_node -> ok; true ->
         % We already know this revision so nothing
@@ -770,10 +789,25 @@ update_doc_replicated(Db, Doc0, _Options) ->
         ?RETURN({ok, []})
     end,
 
+    % Its possible to have a replication with fewer than $revs_limit
+    % revisions which extends an existing branch. To avoid
+    % losing revision history we extract the new node from the
+    % tree and use the combined path after stemming.
+    {[{_, {RevPos, UnstemmedRevs}}], []}
+            = couch_key_tree:get(NewTree, [{RevPos, Rev}]),
+    RevsLimit = fabric2_db:get_revs_limit(Db),
+    Doc1 = Doc0#doc{
+        revs = {RevPos, lists:sublist(UnstemmedRevs, RevsLimit)}
+    },
+    {RevPos, [Rev | NewRevPath]} = Doc1#doc.revs,
+    DocRevInfo1 = DocRevInfo0#{rev_path := NewRevPath},
+
+    % Find any previous revision we knew about for
+    % validation and attachment handling.
     AllLeafsFull = couch_key_tree:get_all_leafs_full(NewTree),
     LeafPath = get_leaf_path(RevPos, Rev, AllLeafsFull),
     PrevRevInfo = find_prev_revinfo(RevPos, LeafPath),
-    Doc1 = prep_and_validate(Db, Doc0, PrevRevInfo),
+    Doc2 = prep_and_validate(Db, Doc1, PrevRevInfo),
 
     % Possible winners are the previous winner and
     % the new DocRevInfo
@@ -783,9 +817,9 @@ update_doc_replicated(Db, Doc0, _Options) ->
     end,
     {NewWinner0, NonWinner} = case Winner == PrevRevInfo of
         true ->
-            {DocRevInfo, not_found};
+            {DocRevInfo1, not_found};
         false ->
-            [W, NW] = fabric2_util:sort_revinfos([Winner, DocRevInfo]),
+            [W, NW] = fabric2_util:sort_revinfos([Winner, DocRevInfo1]),
             {W, NW}
     end,
 
@@ -795,17 +829,14 @@ update_doc_replicated(Db, Doc0, _Options) ->
 
     ok = fabric2_fdb:write_doc(
             Db,
-            Doc1,
+            Doc2,
             NewWinner,
             Winner,
             ToUpdate,
             ToRemove
         ),
 
-    #doc{
-        revs = {NewRevPos, [NewRev | _]}
-    } = Doc1,
-    {ok, {NewRevPos, NewRev}}.
+    {ok, []}.
 
 
 prep_and_validate(Db, Doc, PrevRevInfo) ->
diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index 13f9038..e8c780a 100644
--- a/src/fabric/src/fabric2_fdb.erl
+++ b/src/fabric/src/fabric2_fdb.erl
@@ -128,7 +128,7 @@ create(#{} = Db0, Options) ->
 
         validate_doc_update_funs => [],
         before_doc_update => undefined,
-        after_doc_update => undefined,
+        after_doc_read => undefined,
         % All other db things as we add features,
 
         db_options => Options
diff --git a/src/fabric/src/fabric2_util.erl b/src/fabric/src/fabric2_util.erl
index 09ff876..6edd1bc 100644
--- a/src/fabric/src/fabric2_util.erl
+++ b/src/fabric/src/fabric2_util.erl
@@ -47,12 +47,7 @@ revinfo_to_path(RevInfo, [Rev | Rest]) ->
 
 
 sort_revinfos(RevInfos) ->
-    CmpFun = fun(A, B) ->
-        case rev_sort_key(A) > rev_sort_key(B) of
-            true -> A;
-            false -> B
-        end
-    end,
+    CmpFun = fun(A, B) -> rev_sort_key(A) > rev_sort_key(B) end,
     lists:sort(CmpFun, RevInfos).
 
 
diff --git a/src/fabric/test/fabric2_db_misc_tests.erl b/src/fabric/test/fabric2_db_misc_tests.erl
index 2b6ae5d..83d6a6a 100644
--- a/src/fabric/test/fabric2_db_misc_tests.erl
+++ b/src/fabric/test/fabric2_db_misc_tests.erl
@@ -29,7 +29,9 @@ misc_test_() ->
             fun cleanup/1,
             {with, [
                 fun empty_db_info/1,
-                fun accessors/1
+                fun accessors/1,
+                fun is_system_db/1,
+                fun ensure_full_commit/1
             ]}
         }
     }.
@@ -56,6 +58,34 @@ empty_db_info({DbName, Db, _}) ->
 
 
 accessors({DbName, Db, _}) ->
+    SeqZero = fabric2_util:to_hex(<<0:80>>),
     ?assertEqual(DbName, fabric2_db:name(Db)),
     ?assertEqual(0, fabric2_db:get_instance_start_time(Db)),
-    ?assertEqual(nil, fabric2_db:get_pid(Db)).
+    ?assertEqual(nil, fabric2_db:get_pid(Db)),
+    ?assertEqual(undefined, fabric2_db:get_before_doc_update_fun(Db)),
+    ?assertEqual(undefined, fabric2_db:get_after_doc_read_fun(Db)),
+    ?assertEqual(SeqZero, fabric2_db:get_committed_update_seq(Db)),
+    ?assertEqual(SeqZero, fabric2_db:get_compacted_seq(Db)),
+    ?assertEqual(SeqZero, fabric2_db:get_update_seq(Db)),
+    ?assertEqual(nil, fabric2_db:get_compactor_pid(Db)),
+    ?assertEqual(1000, fabric2_db:get_revs_limit(Db)),
+    ?assertMatch(<<_:32/binary>>, fabric2_db:get_uuid(Db)),
+    ?assertEqual(true, fabric2_db:is_db(Db)),
+    ?assertEqual(false, fabric2_db:is_db(#{})),
+    ?assertEqual(false, fabric2_db:is_partitioned(Db)).
+
+
+is_system_db({DbName, Db, _}) ->
+    ?assertEqual(false, fabric2_db:is_system_db(Db)),
+    ?assertEqual(false, fabric2_db:is_system_db_name("foo")),
+    ?assertEqual(false, fabric2_db:is_system_db_name(DbName)),
+    ?assertEqual(true, fabric2_db:is_system_db_name(<<"_replicator">>)),
+    ?assertEqual(true, fabric2_db:is_system_db_name("_replicator")),
+    ?assertEqual(true, fabric2_db:is_system_db_name(<<"foo/_replicator">>)),
+    ?assertEqual(false, fabric2_db:is_system_db_name(<<"f.o/_replicator">>)),
+    ?assertEqual(false, fabric2_db:is_system_db_name(<<"foo/bar">>)).
+
+
+ensure_full_commit({_, Db, _}) ->
+    ?assertEqual({ok, 0}, fabric2_db:ensure_full_commit(Db)),
+    ?assertEqual({ok, 0}, fabric2_db:ensure_full_commit(Db, 5)).
diff --git a/src/fabric/test/fabric2_doc_crud_tests.erl b/src/fabric/test/fabric2_doc_crud_tests.erl
new file mode 100644
index 0000000..dc0f834
--- /dev/null
+++ b/src/fabric/test/fabric2_doc_crud_tests.erl
@@ -0,0 +1,374 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(fabric2_doc_crud_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+doc_crud_test_() ->
+    {
+        "Test document CRUD operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun open_missing_doc/1,
+                fun create_new_doc/1,
+                fun update_doc_basic/1,
+                fun update_doc_replicated/1,
+                fun update_doc_replicated_add_conflict/1,
+                fun update_doc_replicated_changes_winner/1,
+                fun update_doc_replicated_extension/1,
+                fun update_doc_replicate_existing_rev/1,
+                fun update_winning_conflict_branch/1,
+                fun update_non_winning_conflict_branch/1,
+                fun delete_doc_basic/1,
+                fun delete_changes_winner/1,
+                fun recreate_doc_basic/1,
+                fun conflict_on_create_new_with_rev/1,
+                fun conflict_on_update_with_no_rev/1,
+                fun conflict_on_create_as_deleted/1,
+                fun conflict_on_recreate_as_deleted/1,
+                fun conflict_on_extend_deleted/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), []),
+    {Db, Ctx}.
+
+
+cleanup({Db, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+open_missing_doc({Db, _}) ->
+    ?assertEqual({not_found, missing}, fabric2_db:open_doc(Db, <<"foo">>)).
+
+
+create_new_doc({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {RevPos, Rev}} = fabric2_db:update_doc(Db, Doc),
+    NewDoc = Doc#doc{revs = {RevPos, [Rev]}},
+    ?assertEqual({ok, NewDoc}, fabric2_db:open_doc(Db, Doc#doc.id)).
+
+
+update_doc_basic({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc2#doc{
+        revs = {Pos2, [Rev2, Rev1]}
+    },
+    ?assertEqual({ok, Doc3}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_doc_replicated({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [fabric2_util:uuid(), fabric2_util:uuid()]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc, [replicated_changes]),
+    ?assertEqual({ok, Doc}, fabric2_db:open_doc(Db, Doc#doc.id)).
+
+
+update_doc_replicated_add_conflict({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc1#doc.id)),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_doc_replicated_changes_winner({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc1#doc.id)),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    ?assertEqual({ok, Doc2}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+update_doc_replicated_extension({Db, _}) ->
+    % No sort necessary and avoided on purpose to
+    % demonstrate that this is not sort dependent
+    Rev1 = fabric2_util:uuid(),
+    Rev2 = fabric2_util:uuid(),
+    Rev3 = fabric2_util:uuid(),
+    Rev4 = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {4, [Rev4, Rev3, Rev2]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {4, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    {ok, Doc3} = fabric2_db:open_doc(Db, Doc2#doc.id),
+    ?assertEqual({4, [Rev4, Rev3, Rev2, Rev1]}, Doc3#doc.revs),
+    ?assertEqual(Doc2#doc{revs = undefined}, Doc3#doc{revs = undefined}).
+
+
+update_doc_replicate_existing_rev({Db, _}) ->
+    Rev1 = fabric2_util:uuid(),
+    Rev2 = fabric2_util:uuid(),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    {ok, []} = fabric2_db:update_docs(Db, [Doc1], [replicated_changes]),
+    ?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc1#doc.id)).
+
+
+update_winning_conflict_branch({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    % Update the winning branch
+    Doc3 = Doc1#doc{
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"baz">>, 2}]}
+    },
+    {ok, {3, Rev4}} = fabric2_db:update_doc(Db, Doc3),
+    {ok, Doc4} = fabric2_db:open_doc(Db, Doc3#doc.id),
+    % Assert we've got the correct winner
+    ?assertEqual({3, [Rev4, Rev3, Rev1]}, Doc4#doc.revs),
+    ?assertEqual(Doc3#doc{revs = undefined}, Doc4#doc{revs = undefined}).
+
+
+update_non_winning_conflict_branch({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    % Update the non winning branch
+    Doc3 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"baz">>, 2}]}
+    },
+    {ok, {3, Rev4}} = fabric2_db:update_doc(Db, Doc3),
+    {ok, Doc4} = fabric2_db:open_doc(Db, Doc3#doc.id),
+    % Assert we've got the correct winner
+    ?assertEqual({3, [Rev4, Rev2, Rev1]}, Doc4#doc.revs),
+    ?assertEqual(Doc3#doc{revs = undefined}, Doc4#doc{revs = undefined}).
+
+
+delete_doc_basic({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc2#doc{revs = {Pos2, [Rev2, Rev1]}},
+    ?assertEqual({ok, Doc3}, fabric2_db:open_doc(Db, Doc2#doc.id)).
+
+
+delete_changes_winner({Db, _}) ->
+    [Rev1, Rev2, Rev3] = lists:sort([
+            fabric2_util:uuid(),
+            fabric2_util:uuid(),
+            fabric2_util:uuid()
+        ]),
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        revs = {2, [Rev3, Rev1]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc1, [replicated_changes]),
+    Doc2 = Doc1#doc{
+        revs = {2, [Rev2, Rev1]},
+        body = {[{<<"bar">>, <<"foo">>}]}
+    },
+    {ok, {2, _}} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
+    % Delete the winning branch
+    Doc3 = Doc1#doc{
+        revs = {2, [Rev3, Rev1]},
+        deleted = true,
+        body = {[]}
+    },
+    {ok, {3, _}} = fabric2_db:update_doc(Db, Doc3),
+    ?assertEqual({ok, Doc2}, fabric2_db:open_doc(Db, Doc3#doc.id)).
+
+
+recreate_doc_basic({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc1#doc{
+        revs = {0, []},
+        deleted = false,
+        body = {[{<<"state">>, 3}]}
+    },
+    {ok, {3, Rev3}} = fabric2_db:update_doc(Db, Doc3),
+    {ok, Doc4} = fabric2_db:open_doc(Db, Doc3#doc.id),
+    ?assertEqual({3, [Rev3, Rev2, Rev1]}, Doc4#doc.revs),
+    ?assertEqual(Doc3#doc{revs = undefined}, Doc4#doc{revs = undefined}).
+
+
+conflict_on_create_new_with_rev({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        revs = {1, [fabric2_util:uuid()]},
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc)).
+
+
+conflict_on_update_with_no_rev({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, _} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {0, []},
+        body = {[{<<"state">>, 2}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc2)).
+
+
+conflict_on_create_as_deleted({Db, _}) ->
+    Doc = #doc{
+        id = fabric2_util:uuid(),
+        deleted = true,
+        body = {[{<<"foo">>, <<"bar">>}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc)).
+
+
+conflict_on_recreate_as_deleted({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, _} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc1#doc{
+        revs = {0, []},
+        deleted = true,
+        body = {[{<<"state">>, 3}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc3)).
+
+
+conflict_on_extend_deleted({Db, _}) ->
+    Doc1 = #doc{
+        id = fabric2_util:uuid(),
+        body = {[{<<"state">>, 1}]}
+    },
+    {ok, {Pos1, Rev1}} = fabric2_db:update_doc(Db, Doc1),
+    Doc2 = Doc1#doc{
+        revs = {Pos1, [Rev1]},
+        deleted = true,
+        body = {[{<<"state">>, 2}]}
+    },
+    {ok, {Pos2, Rev2}} = fabric2_db:update_doc(Db, Doc2),
+    Doc3 = Doc1#doc{
+        revs = {Pos2, [Rev2]},
+        deleted = false,
+        body = {[{<<"state">>, 3}]}
+    },
+    ?assertThrow({error, conflict}, fabric2_db:update_doc(Db, Doc3)).
\ No newline at end of file