blob: 8ffbcbc7b0e2279af9a37abb6a1d725d2da6da30 [file] [log] [blame]
% 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").
-include("fabric2.hrl").
-include("fabric2_test.hrl").
doc_crud_test_() ->
{
"Test document CRUD operations",
{
setup,
fun setup/0,
fun cleanup/1,
with([
?TDEF(open_missing_doc),
?TDEF(create_new_doc),
?TDEF(create_ddoc_basic),
?TDEF(create_ddoc_requires_admin),
?TDEF(create_ddoc_requires_validation),
?TDEF(create_ddoc_requires_compilation),
?TDEF(can_create_a_partitioned_ddoc),
?TDEF(update_doc_basic),
?TDEF(update_ddoc_basic),
?TDEF(update_doc_replicated),
?TDEF(update_doc_replicated_add_conflict),
?TDEF(update_doc_replicated_changes_winner),
?TDEF(update_doc_replicated_extension),
?TDEF(update_doc_replicate_existing_rev),
?TDEF(update_winning_conflict_branch),
?TDEF(update_non_winning_conflict_branch),
?TDEF(delete_doc_basic),
?TDEF(delete_changes_winner),
?TDEF(recreate_doc_basic),
?TDEF(conflict_on_create_new_with_rev),
?TDEF(conflict_on_update_with_no_rev),
?TDEF(allow_create_new_as_deleted),
?TDEF(conflict_on_recreate_as_deleted),
?TDEF(conflict_on_extend_deleted),
?TDEF(open_doc_revs_basic),
?TDEF(open_doc_revs_all),
?TDEF(open_doc_revs_latest),
?TDEF(get_missing_revs_basic),
?TDEF(get_missing_revs_on_missing_doc),
?TDEF(open_missing_local_doc),
?TDEF(create_local_doc_basic),
?TDEF(update_local_doc_basic),
?TDEF(delete_local_doc_basic),
?TDEF(recreate_local_doc),
?TDEF(create_local_doc_bad_rev),
?TDEF(create_local_doc_random_rev),
?TDEF(create_a_large_local_doc),
?TDEF(create_2_large_local_docs),
?TDEF(local_doc_with_previous_encoding),
?TDEF(before_doc_update_skips_local_docs),
?TDEF(open_doc_opts)
])
}
}.
setup() ->
Ctx = test_util:start_couch([fabric, couch_js]),
{ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
{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)).
create_ddoc_basic({Db, _}) ->
UUID = fabric2_util:uuid(),
DDocId = <<"_design/", UUID/binary>>,
Doc = #doc{
id = DDocId,
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)).
can_create_a_partitioned_ddoc({Db, _}) ->
UUID = fabric2_util:uuid(),
DDocId = <<"_design/", UUID/binary>>,
Doc = #doc{
id = DDocId,
body =
{[
{<<"options">>, {[{<<"partitioned">>, true}]}},
{<<"views">>,
{[
{<<"foo">>,
{[
{<<"map">>, <<"function(doc) {}">>}
]}}
]}}
]}
},
?assertMatch({ok, {_, _}}, fabric2_db:update_doc(Db, Doc)).
create_ddoc_requires_admin({Db, _}) ->
Db2 = fabric2_db:set_user_ctx(Db, #user_ctx{}),
UUID = fabric2_util:uuid(),
DDocId = <<"_design/", UUID/binary>>,
Doc = #doc{
id = DDocId,
body = {[{<<"foo">>, <<"bar">>}]}
},
?assertThrow({unauthorized, _}, fabric2_db:update_doc(Db2, Doc)).
create_ddoc_requires_validation({Db, _}) ->
UUID = fabric2_util:uuid(),
DDocId = <<"_design/", UUID/binary>>,
Doc = #doc{
id = DDocId,
body =
{[
{<<"views">>,
{[
{<<"foo">>,
{[
{<<"map">>, <<"function(doc) {}">>},
{<<"reduce">>, <<"_not_a_builtin_reduce">>}
]}}
]}}
]}
},
?assertThrow(
{bad_request, invalid_design_doc, _},
fabric2_db:update_doc(Db, Doc)
).
create_ddoc_requires_compilation({Db, _}) ->
UUID = fabric2_util:uuid(),
DDocId = <<"_design/", UUID/binary>>,
Doc = #doc{
id = DDocId,
body =
{[
{<<"language">>, <<"javascript">>},
{<<"views">>,
{[
{<<"foo">>,
{[
{<<"map">>, <<"Hopefully this is invalid JavaScript">>}
]}}
]}}
]}
},
?assertThrow(
{bad_request, compilation_error, _},
fabric2_db:update_doc(Db, Doc)
).
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_ddoc_basic({Db, _}) ->
UUID = fabric2_util:uuid(),
DDocId = <<"_design/", UUID/binary>>,
Doc1 = #doc{
id = DDocId,
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, [deleted])).
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(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(conflict, fabric2_db:update_doc(Db, Doc2)).
allow_create_new_as_deleted({Db, _}) ->
Doc = #doc{
id = fabric2_util:uuid(),
deleted = true,
body = {[{<<"foo">>, <<"bar">>}]}
},
{ok, {1, Rev}} = fabric2_db:update_doc(Db, Doc),
?assertEqual({not_found, deleted}, fabric2_db:open_doc(Db, Doc#doc.id)),
Doc1 = Doc#doc{
revs = {1, [Rev]}
},
?assertEqual({ok, Doc1}, fabric2_db:open_doc(Db, Doc#doc.id, [deleted])),
% Only works when the document has never existed to match CouchDB 3.x
% behavior
?assertThrow(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(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(conflict, fabric2_db:update_doc(Db, Doc3)).
open_doc_revs_basic({Db, _}) ->
[Rev1, Rev2, Rev3] = lists:sort([
fabric2_util:uuid(),
fabric2_util:uuid(),
fabric2_util:uuid()
]),
DocId = fabric2_util:uuid(),
Doc1 = #doc{
id = DocId,
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]),
{ok, [{ok, Doc3}]} = fabric2_db:open_doc_revs(Db, DocId, [{2, Rev3}], []),
?assertEqual(Doc1, Doc3),
{ok, [{ok, Doc4}]} = fabric2_db:open_doc_revs(Db, DocId, [{2, Rev2}], []),
?assertEqual(Doc2, Doc4),
Revs = [{2, Rev3}, {2, Rev2}, {1, Rev1}],
{ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, Revs, []),
?assert(length(Docs) == 3),
?assert(lists:member({ok, Doc1}, Docs)),
?assert(lists:member({ok, Doc2}, Docs)),
?assert(lists:member({{not_found, missing}, {1, Rev1}}, Docs)),
% Make sure crazy madeup revisions are accepted
MissingRevs = [{5, fabric2_util:uuid()}, {1, fabric2_util:uuid()}],
{ok, NFMissing} = fabric2_db:open_doc_revs(Db, DocId, MissingRevs, []),
?assertEqual(2, length(NFMissing)),
lists:foreach(
fun(MR) ->
?assert(lists:member({{not_found, missing}, MR}, NFMissing))
end,
MissingRevs
).
open_doc_revs_all({Db, _}) ->
[Rev1, Rev2, Rev3] = lists:sort([
fabric2_util:uuid(),
fabric2_util:uuid(),
fabric2_util:uuid()
]),
DocId = fabric2_util:uuid(),
Doc1 = #doc{
id = DocId,
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]),
{ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, all, []),
?assert(length(Docs) == 2),
?assert(lists:member({ok, Doc1}, Docs)),
?assert(lists:member({ok, Doc2}, Docs)).
open_doc_revs_latest({Db, _}) ->
[Rev1, Rev2, Rev3] = lists:sort([
fabric2_util:uuid(),
fabric2_util:uuid(),
fabric2_util:uuid()
]),
DocId = fabric2_util:uuid(),
Doc1 = #doc{
id = DocId,
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]),
Opts = [latest],
{ok, [{ok, Doc3}]} = fabric2_db:open_doc_revs(Db, DocId, [{2, Rev3}], Opts),
?assertEqual(Doc1, Doc3),
{ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, [{1, Rev1}], Opts),
?assert(length(Docs) == 2),
?assert(lists:member({ok, Doc1}, Docs)),
?assert(lists:member({ok, Doc2}, Docs)).
get_missing_revs_basic({Db, _}) ->
[Rev1, Rev2, Rev3] = lists:sort([
fabric2_util:uuid(),
fabric2_util:uuid(),
fabric2_util:uuid()
]),
DocId = fabric2_util:uuid(),
Doc1 = #doc{
id = DocId,
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]),
% Check that we can find all revisions
AllRevs = [{1, Rev1}, {2, Rev2}, {2, Rev3}],
?assertEqual(
{ok, []},
fabric2_db:get_missing_revs(Db, [{DocId, AllRevs}])
),
% Check that a missing revision is found with no possible ancestors
MissingRev = {2, fabric2_util:uuid()},
?assertEqual(
{ok, [{DocId, [MissingRev], []}]},
fabric2_db:get_missing_revs(Db, [{DocId, [MissingRev]}])
),
% Check that only a missing rev is returned
?assertEqual(
{ok, [{DocId, [MissingRev], []}]},
fabric2_db:get_missing_revs(Db, [{DocId, [MissingRev | AllRevs]}])
),
% Check that we can find possible ancestors
MissingWithAncestors = {4, fabric2_util:uuid()},
PossibleAncestors = [{2, Rev2}, {2, Rev3}],
?assertEqual(
{ok, [{DocId, [MissingWithAncestors], PossibleAncestors}]},
fabric2_db:get_missing_revs(Db, [{DocId, [MissingWithAncestors]}])
).
get_missing_revs_on_missing_doc({Db, _}) ->
Revs = lists:sort([
couch_doc:rev_to_str({1, fabric2_util:uuid()}),
couch_doc:rev_to_str({2, fabric2_util:uuid()}),
couch_doc:rev_to_str({800, fabric2_util:uuid()})
]),
DocId = fabric2_util:uuid(),
{ok, Resp} = fabric2_db:get_missing_revs(Db, [{DocId, Revs}]),
?assertMatch([{DocId, [_ | _], []}], Resp),
[{DocId, Missing, _}] = Resp,
MissingStrs = [couch_doc:rev_to_str(Rev) || Rev <- Missing],
?assertEqual(Revs, lists:sort(MissingStrs)).
open_missing_local_doc({Db, _}) ->
?assertEqual(
{not_found, missing},
fabric2_db:open_doc(Db, <<"_local/foo">>, [])
).
create_local_doc_basic({Db, _}) ->
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Doc1 = #doc{
id = LDocId,
revs = {0, []},
deleted = false,
body = {[{<<"ohai">>, <<"there">>}]}
},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
{ok, Doc2} = fabric2_db:open_doc(Db, Doc1#doc.id, []),
?assertEqual(Doc1#doc{revs = {0, [<<"1">>]}}, Doc2).
update_local_doc_basic({Db, _}) ->
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Doc1 = #doc{
id = LDocId,
revs = {0, []},
deleted = false,
body = {[{<<"ohai">>, <<"there">>}]}
},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
Doc2 = Doc1#doc{
revs = {0, [<<"1">>]},
body = {[{<<"whiz">>, <<"bang">>}]}
},
?assertEqual({ok, {0, <<"2">>}}, fabric2_db:update_doc(Db, Doc2)),
{ok, Doc3} = fabric2_db:open_doc(Db, Doc1#doc.id, []),
?assertEqual(Doc2#doc{revs = {0, [<<"2">>]}}, Doc3).
delete_local_doc_basic({Db, _}) ->
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Doc1 = #doc{
id = LDocId,
revs = {0, []},
deleted = false,
body = {[{<<"ohai">>, <<"there">>}]}
},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
Doc2 = Doc1#doc{
revs = {0, [<<"1">>]},
deleted = true,
body = {[]}
},
?assertEqual({ok, {0, <<"0">>}}, fabric2_db:update_doc(Db, Doc2)),
?assertEqual(
{not_found, missing},
fabric2_db:open_doc(Db, LDocId)
).
recreate_local_doc({Db, _}) ->
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Doc1 = #doc{
id = LDocId,
revs = {0, []},
deleted = false,
body = {[{<<"ohai">>, <<"there">>}]}
},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
Doc2 = Doc1#doc{
revs = {0, [<<"1">>]},
deleted = true,
body = {[]}
},
?assertEqual({ok, {0, <<"0">>}}, fabric2_db:update_doc(Db, Doc2)),
?assertEqual(
{not_found, missing},
fabric2_db:open_doc(Db, LDocId)
),
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
{ok, Doc3} = fabric2_db:open_doc(Db, LDocId),
?assertEqual(Doc1#doc{revs = {0, [<<"1">>]}}, Doc3).
create_local_doc_bad_rev({Db, _}) ->
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Doc1 = #doc{
id = LDocId,
revs = {0, [<<"not a number">>]}
},
?assertThrow(<<"Invalid rev format">>, fabric2_db:update_doc(Db, Doc1)),
Doc2 = Doc1#doc{
revs = bad_bad_rev_roy_brown
},
?assertThrow(<<"Invalid rev format">>, fabric2_db:update_doc(Db, Doc2)).
create_local_doc_random_rev({Db, _}) ->
% Local docs don't care what rev is passed as long
% as long as its a number.
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Doc1 = #doc{
id = LDocId,
revs = {0, [<<"42">>]},
body = {[{<<"state">>, 1}]}
},
?assertEqual({ok, {0, <<"43">>}}, fabric2_db:update_doc(Db, Doc1)),
{ok, Doc2} = fabric2_db:open_doc(Db, LDocId, []),
?assertEqual(Doc1#doc{revs = {0, [<<"43">>]}}, Doc2),
Doc3 = Doc1#doc{
revs = {0, [<<"1234567890">>]},
body = {[{<<"state">>, 2}]}
},
?assertEqual({ok, {0, <<"1234567891">>}}, fabric2_db:update_doc(Db, Doc3)),
{ok, Doc4} = fabric2_db:open_doc(Db, LDocId, []),
?assertEqual(Doc3#doc{revs = {0, [<<"1234567891">>]}}, Doc4),
Doc5 = Doc1#doc{
revs = {0, [<<"1">>]},
body = {[{<<"state">>, 3}]}
},
?assertEqual({ok, {0, <<"2">>}}, fabric2_db:update_doc(Db, Doc5)),
{ok, Doc6} = fabric2_db:open_doc(Db, LDocId, []),
?assertEqual(Doc5#doc{revs = {0, [<<"2">>]}}, Doc6).
create_a_large_local_doc({Db, _}) ->
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Body = <<<<"x">> || _ <- lists:seq(1, 300000)>>,
Doc1 = #doc{
id = LDocId,
revs = {0, []},
body = Body
},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
{ok, Doc2} = fabric2_db:open_doc(Db, Doc1#doc.id, []),
?assertEqual(Doc1#doc{revs = {0, [<<"1">>]}}, Doc2),
% Read via fold_local_docs
{ok, Result} = fabric2_db:fold_local_docs(
Db,
fun(Data, Acc) ->
case Data of
{row, [{id, DocId} | _]} when LDocId =:= DocId ->
{ok, [Data | Acc]};
_ ->
{ok, Acc}
end
end,
[],
[]
),
?assertEqual(
[
{row, [
{id, LDocId},
{key, LDocId},
{value, {[{rev, <<"0-1">>}]}}
]}
],
Result
).
create_2_large_local_docs({Db, _}) ->
% Create a large doc then overwrite with a smaller one. The reason is to
% ensure the previous one correctly clears its range before writting the
% new smaller one it its place.
UUID = fabric2_util:uuid(),
LDocId = <<?LOCAL_DOC_PREFIX, UUID/binary>>,
Body1 = <<<<"x">> || _ <- lists:seq(1, 400000)>>,
Body2 = <<<<"y">> || _ <- lists:seq(1, 150000)>>,
Doc1 = #doc{
id = LDocId,
revs = {0, []},
body = Body1
},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc1)),
Doc2 = Doc1#doc{body = Body2},
?assertEqual({ok, {0, <<"1">>}}, fabric2_db:update_doc(Db, Doc2)),
{ok, Doc3} = fabric2_db:open_doc(Db, LDocId, []),
?assertEqual(Doc2#doc{revs = {0, [<<"1">>]}}, Doc3).
local_doc_with_previous_encoding({Db, _}) ->
#{db_prefix := DbPrefix} = Db,
Id = <<"_local/old_doc">>,
Body = {[{<<"x">>, 5}]},
Rev = <<"1">>,
Key = erlfdb_tuple:pack({?DB_LOCAL_DOCS, Id}, DbPrefix),
fabric2_fdb:transactional(Db, fun(TxDb) ->
#{tx := Tx} = TxDb,
Term = term_to_binary({Rev, Body}, [{minor_version, 1}]),
ok = erlfdb:set(Tx, Key, Term)
end),
% Read old doc
{ok, Doc1} = fabric2_db:open_doc(Db, Id, []),
?assertEqual({0, [<<"1">>]}, Doc1#doc.revs),
?assertEqual({[{<<"x">>, 5}]}, Doc1#doc.body),
% Read via fold_local_docs.
{ok, Result} = fabric2_db:fold_local_docs(
Db,
fun(Data, Acc) ->
case Data of
{row, [{id, DocId} | _]} when Id =:= DocId ->
{ok, [Data | Acc]};
_ ->
{ok, Acc}
end
end,
[],
[]
),
?assertEqual(
[
{row, [
{id, Id},
{key, Id},
{value, {[{rev, <<"0-1">>}]}}
]}
],
Result
),
% Update doc
NewBody = {[{<<"y">>, 6}]},
Doc2 = Doc1#doc{body = NewBody},
?assertEqual({ok, {0, <<"2">>}}, fabric2_db:update_doc(Db, Doc2)),
{ok, Doc3} = fabric2_db:open_doc(Db, Doc2#doc.id, []),
?assertEqual({0, [<<"2">>]}, Doc3#doc.revs),
?assertEqual(NewBody, Doc3#doc.body),
% Old doc now has only the rev number in it
<<255, OldDocBin/binary>> = fabric2_fdb:transactional(Db, fun(TxDb) ->
#{tx := Tx} = TxDb,
erlfdb:wait(erlfdb:get(Tx, Key))
end),
Unpacked = erlfdb_tuple:unpack(OldDocBin),
?assertMatch({?CURR_LDOC_FORMAT, <<"2">>, _}, Unpacked).
before_doc_update_skips_local_docs({Db0, _}) ->
BduFun = fun(Doc, _, _) ->
Doc#doc{body = {[<<"bdu_was_here">>, true]}}
end,
Db = Db0#{before_doc_update := BduFun},
LDoc1 = #doc{id = <<"_local/ldoc1">>},
Doc1 = #doc{id = <<"doc1">>},
?assertMatch({ok, {_, _}}, fabric2_db:update_doc(Db, LDoc1)),
?assertMatch({ok, {_, _}}, fabric2_db:update_doc(Db, Doc1)),
{ok, LDoc2} = fabric2_db:open_doc(Db, LDoc1#doc.id),
{ok, Doc2} = fabric2_db:open_doc(Db, Doc1#doc.id),
?assertEqual({[]}, LDoc2#doc.body),
?assertEqual({[<<"bdu_was_here">>, true]}, Doc2#doc.body).
open_doc_opts({Db, _}) ->
% Build out state so that we can exercise each doc
% open option. This requires a live revision with
% an attachment, a conflict, and a deleted conflict.
DocId = couch_uuids:random(),
Att1 = couch_att:new([
{name, <<"foo.txt">>},
{type, <<"application/octet-stream">>},
{att_len, 6},
{data, <<"foobar">>},
{encoding, identity},
{md5, <<>>}
]),
Doc1A = #doc{
id = DocId,
atts = [Att1]
},
{ok, {Pos1, Rev1A}} = fabric2_db:update_doc(Db, Doc1A),
Att2 = couch_att:store(
[
{data, stub},
{revpos, 1}
],
Att1
),
Doc1B = Doc1A#doc{
revs = {Pos1, [Rev1A]},
atts = [Att2]
},
{ok, {Pos2, Rev1B}} = fabric2_db:update_doc(Db, Doc1B),
Rev2 = crypto:strong_rand_bytes(16),
Rev3 = crypto:strong_rand_bytes(16),
Rev4 = crypto:strong_rand_bytes(16),
% Create a live conflict
Doc2 = #doc{
id = DocId,
revs = {1, [Rev2]}
},
{ok, _} = fabric2_db:update_doc(Db, Doc2, [replicated_changes]),
% Create a deleted conflict
Doc3 = #doc{
id = DocId,
revs = {1, [Rev3]}
},
{ok, _} = fabric2_db:update_doc(Db, Doc3, [replicated_changes]),
Doc4 = #doc{
id = DocId,
revs = {2, [Rev4, Rev3]},
deleted = true
},
{ok, _} = fabric2_db:update_doc(Db, Doc4, [replicated_changes]),
OpenOpts1 = [
revs_info,
conflicts,
deleted_conflicts,
local_seq,
{atts_since, [{Pos1, Rev1A}]}
],
{ok, OpenedDoc1} = fabric2_db:open_doc(Db, DocId, OpenOpts1),
#doc{
id = DocId,
revs = {2, [Rev1B, Rev1A]},
atts = [Att3],
meta = Meta
} = OpenedDoc1,
?assertEqual(stub, couch_att:fetch(data, Att3)),
?assertEqual(
{revs_info, Pos2, [{Rev1B, available}, {Rev1A, missing}]},
lists:keyfind(revs_info, 1, Meta)
),
?assertEqual(
{conflicts, [{1, Rev2}]},
lists:keyfind(conflicts, 1, Meta)
),
?assertEqual(
{deleted_conflicts, [{2, Rev4}]},
lists:keyfind(deleted_conflicts, 1, Meta)
),
?assertMatch({_, <<_/binary>>}, lists:keyfind(local_seq, 1, Meta)),
% Empty atts_since list
{ok, OpenedDoc2} = fabric2_db:open_doc(Db, DocId, [{atts_since, []}]),
#doc{atts = [Att4]} = OpenedDoc2,
?assertNotEqual(stub, couch_att:fetch(data, Att4)),
% Missing ancestor
Rev5 = crypto:strong_rand_bytes(16),
OpenOpts2 = [{atts_since, [{5, Rev5}]}],
{ok, OpenedDoc3} = fabric2_db:open_doc(Db, DocId, OpenOpts2),
#doc{atts = [Att5]} = OpenedDoc3,
?assertNotEqual(stub, couch_att:fetch(data, Att5)).