eiri closed pull request #1683: Fixes for _local doc update and _bulk_docs 
operations with new_edits false
URL: https://github.com/apache/couchdb/pull/1683
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index f95c3e7948..d46b5bbf28 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -420,19 +420,16 @@ 
db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>], user_ctx=Ctx}=Req,
     _ ->
         Options = [{user_ctx,Ctx}, {w,W}]
     end,
+    Docs = lists:map(fun(JsonObj) ->
+        Doc = couch_doc:from_json_obj_validate(JsonObj),
+        validate_attachment_names(Doc),
+        case Doc#doc.id of
+            <<>> -> Doc#doc{id = couch_uuids:new()};
+            _ -> Doc
+        end
+    end, DocsArray),
     case couch_util:get_value(<<"new_edits">>, JsonProps, true) of
     true ->
-        Docs = lists:map(
-            fun(JsonObj) ->
-                Doc = couch_doc:from_json_obj_validate(JsonObj),
-                validate_attachment_names(Doc),
-                Id = case Doc#doc.id of
-                    <<>> -> couch_uuids:new();
-                    Id0 -> Id0
-                end,
-                Doc#doc{id=Id}
-            end,
-            DocsArray),
         Options2 =
         case couch_util:get_value(<<"all_or_nothing">>, JsonProps) of
         true  -> [all_or_nothing|Options];
@@ -455,8 +452,6 @@ 
db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>], user_ctx=Ctx}=Req,
             send_json(Req, 417, ErrorsJson)
         end;
     false ->
-        Docs = [couch_doc:from_json_obj_validate(JsonObj) || JsonObj <- 
DocsArray],
-        [validate_attachment_names(D) || D <- Docs],
         case fabric:update_docs(Db, Docs, [replicated_changes|Options]) of
         {ok, Errors} ->
             ErrorsJson = lists:map(fun update_doc_result_to_json/1, Errors),
diff --git a/src/couch/src/couch_db.erl b/src/couch/src/couch_db.erl
index f65900223f..9d6a5dc45b 100644
--- a/src/couch/src/couch_db.erl
+++ b/src/couch/src/couch_db.erl
@@ -1114,69 +1114,35 @@ doc_tag(#doc{meta=Meta}) ->
     end.
 
 update_docs(Db, Docs0, Options, replicated_changes) ->
-    increment_stat(Db, [couchdb, database_writes]),
     Docs = tag_docs(Docs0),
-    DocBuckets = before_docs_update(Db, group_alike_docs(Docs)),
-
-    case (Db#db.validate_doc_funs /= []) orelse
-        lists:any(
-            fun(#doc{id= <<?DESIGN_DOC_PREFIX, _/binary>>}) -> true;
-            (#doc{atts=Atts}) ->
-                Atts /= []
-            end, Docs) of
-    true ->
-        Ids = [Id || [#doc{id=Id}|_] <- DocBuckets],
-        ExistingDocs = get_full_doc_infos(Db, Ids),
 
-        {DocBuckets2, DocErrors} =
-                prep_and_validate_replicated_updates(Db, DocBuckets, 
ExistingDocs, [], []),
-        DocBuckets3 = [Bucket || [_|_]=Bucket <- DocBuckets2]; % remove empty 
buckets
-    false ->
-        DocErrors = [],
-        DocBuckets3 = DocBuckets
+    PrepValidateFun = fun(Db0, DocBuckets0, ExistingDocInfos) ->
+        prep_and_validate_replicated_updates(Db0, DocBuckets0,
+            ExistingDocInfos, [], [])
     end,
-    DocBuckets4 = [[doc_flush_atts(Db, check_dup_atts(Doc))
-            || Doc <- Bucket] || Bucket <- DocBuckets3],
-    {ok, []} = write_and_commit(Db, DocBuckets4, [], [merge_conflicts | 
Options]),
+
+    {ok, DocBuckets, NonRepDocs, DocErrors}
+        = before_docs_update(Db, Docs, PrepValidateFun),
+
+    DocBuckets2 = [[doc_flush_atts(Db, check_dup_atts(Doc))
+            || Doc <- Bucket] || Bucket <- DocBuckets],
+    {ok, _} = write_and_commit(Db, DocBuckets2,
+        NonRepDocs, [merge_conflicts | Options]),
     {ok, DocErrors};
 
 update_docs(Db, Docs0, Options, interactive_edit) ->
-    increment_stat(Db, [couchdb, database_writes]),
-    AllOrNothing = lists:member(all_or_nothing, Options),
     Docs = tag_docs(Docs0),
 
-    % Separate _local docs from normal docs
-    IsLocal = fun
-        (#doc{id= <<?LOCAL_DOC_PREFIX, _/binary>>}) -> true;
-        (_) -> false
+    AllOrNothing = lists:member(all_or_nothing, Options),
+    PrepValidateFun = fun(Db0, DocBuckets0, ExistingDocInfos) ->
+        prep_and_validate_updates(Db0, DocBuckets0, ExistingDocInfos,
+            AllOrNothing, [], [])
     end,
-    {NonRepDocs, Docs2} = lists:partition(IsLocal, Docs),
 
-    DocBuckets = before_docs_update(Db, group_alike_docs(Docs2)),
-
-    case (Db#db.validate_doc_funs /= []) orelse
-        lists:any(
-            fun(#doc{id= <<?DESIGN_DOC_PREFIX, _/binary>>}) ->
-                true;
-            (#doc{atts=Atts}) ->
-                Atts /= []
-            end, Docs2) of
-    true ->
-        % lookup the doc by id and get the most recent
-        Ids = [Id || [#doc{id=Id}|_] <- DocBuckets],
-        ExistingDocInfos = get_full_doc_infos(Db, Ids),
-
-        {DocBucketsPrepped, PreCommitFailures} = prep_and_validate_updates(Db,
-                DocBuckets, ExistingDocInfos, AllOrNothing, [], []),
-
-        % strip out any empty buckets
-        DocBuckets2 = [Bucket || [_|_] = Bucket <- DocBucketsPrepped];
-    false ->
-        PreCommitFailures = [],
-        DocBuckets2 = DocBuckets
-    end,
+    {ok, DocBuckets, NonRepDocs, DocErrors}
+        = before_docs_update(Db, Docs, PrepValidateFun),
 
-    if (AllOrNothing) and (PreCommitFailures /= []) ->
+    if (AllOrNothing) and (DocErrors /= []) ->
         RefErrorDict = dict:from_list([{doc_tag(Doc), Doc} || Doc <- Docs]),
         {aborted, lists:map(fun({Ref, Error}) ->
             #doc{id=Id,revs={Start,RevIds}} = dict:fetch(Ref, RefErrorDict),
@@ -1184,21 +1150,22 @@ update_docs(Db, Docs0, Options, interactive_edit) ->
                 {Pos, [RevId | _]} -> {{Id, {Pos, RevId}}, Error};
                 {0, []} -> {{Id, {0, <<>>}}, Error}
             end
-        end, PreCommitFailures)};
+        end, DocErrors)};
     true ->
         Options2 = if AllOrNothing -> [merge_conflicts];
                 true -> [] end ++ Options,
-        DocBuckets3 = [[
+        DocBuckets2 = [[
                 doc_flush_atts(Db, set_new_att_revpos(
                         check_dup_atts(Doc)))
-                || Doc <- B] || B <- DocBuckets2],
-        {DocBuckets4, IdRevs} = new_revs(DocBuckets3, [], []),
+                || Doc <- B] || B <- DocBuckets],
+        {DocBuckets3, IdRevs} = new_revs(DocBuckets2, [], []),
 
-        {ok, CommitResults} = write_and_commit(Db, DocBuckets4, NonRepDocs, 
Options2),
+        {ok, CommitResults} = write_and_commit(Db, DocBuckets3,
+            NonRepDocs, Options2),
 
         ResultsDict = lists:foldl(fun({Key, Resp}, ResultsAcc) ->
             dict:store(Key, Resp, ResultsAcc)
-        end, dict:from_list(IdRevs), CommitResults ++ PreCommitFailures),
+        end, dict:from_list(IdRevs), CommitResults ++ DocErrors),
         {ok, lists:map(fun(Doc) ->
             dict:fetch(doc_tag(Doc), ResultsDict)
         end, Docs)}
@@ -1316,13 +1283,42 @@ prepare_doc_summaries(Db, BucketList) ->
         Bucket) || Bucket <- BucketList].
 
 
-before_docs_update(#db{} = Db, BucketList) ->
-    [lists:map(
-            fun(Doc) ->
-                DocWithBody = couch_doc:with_ejson_body(Doc),
-                couch_db_plugin:before_doc_update(Db, DocWithBody)
-            end,
-        Bucket) || Bucket <- BucketList].
+before_docs_update(#db{validate_doc_funs = VDFuns} = Db, Docs, PVFun) ->
+    increment_stat(Db, [couchdb, database_writes]),
+
+    % Separate _local docs from normal docs
+    IsLocal = fun
+        (#doc{id= <<?LOCAL_DOC_PREFIX, _/binary>>}) -> true;
+        (_) -> false
+    end,
+    {NonRepDocs, Docs2} = lists:partition(IsLocal, Docs),
+
+    BucketList = group_alike_docs(Docs2),
+
+    DocBuckets = lists:map(fun(Bucket) ->
+        lists:map(fun(Doc) ->
+            DocWithBody = couch_doc:with_ejson_body(Doc),
+            couch_db_plugin:before_doc_update(Db, DocWithBody)
+        end, Bucket)
+    end, BucketList),
+
+    ValidatePred = fun
+        (#doc{id = <<?DESIGN_DOC_PREFIX, _/binary>>}) -> true;
+        (#doc{atts = Atts}) -> Atts /= []
+    end,
+
+    case (VDFuns /= []) orelse lists:any(ValidatePred, Docs2) of
+        true ->
+            % lookup the doc by id and get the most recent
+            Ids = [Id || [#doc{id = Id} | _] <- DocBuckets],
+            ExistingDocs = get_full_doc_infos(Db, Ids),
+            {DocBuckets2, DocErrors} = PVFun(Db, DocBuckets, ExistingDocs),
+             % remove empty buckets
+            DocBuckets3 = [Bucket || Bucket <- DocBuckets2, Bucket /= []],
+            {ok, DocBuckets3, NonRepDocs, DocErrors};
+        false ->
+            {ok, DocBuckets, NonRepDocs, []}
+    end.
 
 
 set_new_att_revpos(#doc{revs={RevPos,_Revs},atts=Atts0}=Doc) ->
diff --git a/src/couch/src/couch_db_updater.erl 
b/src/couch/src/couch_db_updater.erl
index 52a4d2f1ba..87301d2d8b 100644
--- a/src/couch/src/couch_db_updater.erl
+++ b/src/couch/src/couch_db_updater.erl
@@ -627,28 +627,31 @@ update_docs_int(Db, DocsList, LocalDocs, MergeConflicts, 
FullCommit) ->
 
 
 update_local_doc_revs(Docs) ->
-    lists:map(fun({Client, NewDoc}) ->
-        #doc{
-            deleted = Delete,
-            revs = {0, PrevRevs}
-        } = NewDoc,
-        case PrevRevs of
-            [RevStr | _] ->
-                PrevRev = binary_to_integer(RevStr);
-            [] ->
-                PrevRev = 0
-        end,
-        NewRev = case Delete of
-            false ->
-                PrevRev + 1;
-            true  ->
-                0
-        end,
-        send_result(Client, NewDoc, {ok, {0, integer_to_binary(NewRev)}}),
-        NewDoc#doc{
-            revs = {0, [NewRev]}
-        }
-    end, Docs).
+    lists:foldl(fun({Client, Doc}, Acc) ->
+        case increment_local_doc_revs(Doc) of
+            {ok, #doc{revs = {0, [NewRev]}} = NewDoc} ->
+                send_result(Client, Doc, {ok, {0, integer_to_binary(NewRev)}}),
+                [NewDoc | Acc];
+            {error, Error} ->
+                send_result(Client, Doc, {error, Error}),
+                Acc
+        end
+    end, [], Docs).
+
+
+increment_local_doc_revs(#doc{deleted = true} = Doc) ->
+    {ok, Doc#doc{revs = {0, [0]}}};
+increment_local_doc_revs(#doc{revs = {0, []}} = Doc) ->
+    {ok, Doc#doc{revs = {0, [1]}}};
+increment_local_doc_revs(#doc{revs = {0, [RevStr | _]}} = Doc) ->
+    try
+        PrevRev = binary_to_integer(RevStr),
+        {ok, Doc#doc{revs = {0, [PrevRev + 1]}}}
+    catch error:badarg ->
+        {error, <<"Invalid rev format">>}
+    end;
+increment_local_doc_revs(#doc{}) ->
+    {error, <<"Invalid rev format">>}.
 
 
 purge_docs(Db, []) ->
@@ -808,3 +811,64 @@ hibernate_if_no_idle_limit() ->
         Timeout when is_integer(Timeout) ->
             Timeout
     end.
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+
+update_local_doc_revs_test_() ->
+    {inparallel, [
+        {"Test local doc with valid rev", fun t_good_local_doc/0},
+        {"Test local doc with invalid rev", fun t_bad_local_doc/0},
+        {"Test deleted local doc", fun t_dead_local_doc/0}
+    ]}.
+
+
+t_good_local_doc() ->
+    Doc = #doc{
+        id = <<"_local/alice">>,
+        revs = {0, [<<"1">>]},
+        meta = [{ref, make_ref()}]
+    },
+    [NewDoc] = update_local_doc_revs([{self(), Doc}]),
+    ?assertEqual({0, [2]}, NewDoc#doc.revs),
+    {ok, Result} = receive_result(Doc),
+    ?assertEqual({ok,{0,<<"2">>}}, Result).
+
+
+t_bad_local_doc() ->
+    lists:foreach(fun(BadRevs) ->
+        Doc = #doc{
+            id = <<"_local/alice">>,
+            revs = BadRevs,
+            meta = [{ref, make_ref()}]
+        },
+        NewDocs = update_local_doc_revs([{self(), Doc}]),
+        ?assertEqual([], NewDocs),
+        {ok, Result} = receive_result(Doc),
+        ?assertEqual({error,<<"Invalid rev format">>}, Result)
+    end, [{0, [<<"a">>]}, {1, [<<"1">>]}]).
+
+
+
+t_dead_local_doc() ->
+    Doc = #doc{
+        id = <<"_local/alice">>,
+        revs = {0, [<<"122">>]},
+        deleted = true,
+        meta = [{ref, make_ref()}]
+    },
+    [NewDoc] = update_local_doc_revs([{self(), Doc}]),
+    ?assertEqual({0, [0]}, NewDoc#doc.revs),
+    {ok, Result} = receive_result(Doc),
+    ?assertEqual({ok,{0,<<"0">>}}, Result).
+
+
+receive_result(#doc{meta = Meta}) ->
+    Ref = couch_util:get_value(ref, Meta),
+    receive
+        {result, _, {Ref, Result}} -> {ok, Result}
+    end.
+
+-endif.
diff --git a/src/couch/test/couchdb_update_conflicts_tests.erl 
b/src/couch/test/couchdb_update_conflicts_tests.erl
index 09c2834a8e..e92c738561 100644
--- a/src/couch/test/couchdb_update_conflicts_tests.erl
+++ b/src/couch/test/couchdb_update_conflicts_tests.erl
@@ -17,6 +17,7 @@
 
 -define(i2l(I), integer_to_list(I)).
 -define(DOC_ID, <<"foobar">>).
+-define(LOCAL_DOC_ID, <<"_local/foobar">>).
 -define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]).
 -define(TIMEOUT, 20000).
 
@@ -52,7 +53,7 @@ view_indexes_cleanup_test_() ->
             fun start/0, fun test_util:stop_couch/1,
             [
                 concurrent_updates(),
-                couchdb_188()
+                bulk_docs_updates()
             ]
         }
     }.
@@ -68,13 +69,17 @@ concurrent_updates()->
         }
     }.
 
-couchdb_188()->
+bulk_docs_updates()->
     {
-        "COUCHDB-188",
+        "Bulk docs updates",
         {
             foreach,
             fun setup/0, fun teardown/1,
-            [fun should_bulk_create_delete_doc/1]
+            [
+                fun should_bulk_create_delete_doc/1,
+                fun should_bulk_create_local_doc/1,
+                fun should_ignore_invalid_local_doc/1
+            ]
         }
     }.
 
@@ -91,6 +96,12 @@ should_concurrently_update_doc(NumClients, {DbName, 
InitRev})->
 should_bulk_create_delete_doc({DbName, InitRev})->
     ?_test(bulk_delete_create(DbName, InitRev)).
 
+should_bulk_create_local_doc({DbName, _})->
+    ?_test(bulk_create_local_doc(DbName)).
+
+should_ignore_invalid_local_doc({DbName, _})->
+    ?_test(ignore_invalid_local_doc(DbName)).
+
 
 concurrent_doc_update(NumClients, DbName, InitRev) ->
     Clients = lists:map(
@@ -157,10 +168,10 @@ ensure_in_single_revision_leaf(DbName) ->
 
     [{ok, Doc2}] = Leaves,
     ?assertEqual(Doc, Doc2).
-    
+
 bulk_delete_create(DbName, InitRev) ->
     {ok, Db} = couch_db:open_int(DbName, []),
-    
+
     DeletedDoc = couch_doc:from_json_obj({[
         {<<"_id">>, ?DOC_ID},
         {<<"_rev">>, InitRev},
@@ -176,7 +187,7 @@ bulk_delete_create(DbName, InitRev) ->
 
     ?assertEqual(2, length([ok || {ok, _} <- Results])),
     [{ok, Rev1}, {ok, Rev2}] = Results,
-    
+
     {ok, Db2} = couch_db:open_int(DbName, []),
     {ok, [{ok, Doc1}]} = couch_db:open_doc_revs(
         Db2, ?DOC_ID, [Rev1], [conflicts, deleted_conflicts]),
@@ -214,6 +225,45 @@ bulk_delete_create(DbName, InitRev) ->
     ?assertEqual(3, element(1, Rev2)).
 
 
+bulk_create_local_doc(DbName) ->
+    {ok, Db} = couch_db:open_int(DbName, []),
+
+    LocalDoc = couch_doc:from_json_obj({[
+        {<<"_id">>, ?LOCAL_DOC_ID},
+        {<<"_rev">>, <<"0-1">>}
+    ]}),
+
+    {ok, Results} = couch_db:update_docs(Db, [LocalDoc],
+        [], replicated_changes),
+    ok = couch_db:close(Db),
+    ?assertEqual([], Results),
+
+    {ok, Db2} = couch_db:open_int(DbName, []),
+    {ok, LocalDoc1} = couch_db:open_doc_int(Db2, ?LOCAL_DOC_ID, []),
+    ok = couch_db:close(Db2),
+    ?assertEqual(?LOCAL_DOC_ID, LocalDoc1#doc.id),
+    ?assertEqual({0, [<<"2">>]}, LocalDoc1#doc.revs).
+
+
+ignore_invalid_local_doc(DbName) ->
+    {ok, Db} = couch_db:open_int(DbName, []),
+
+    LocalDoc = couch_doc:from_json_obj({[
+        {<<"_id">>, ?LOCAL_DOC_ID},
+        {<<"_rev">>, <<"0-abcdef">>}
+    ]}),
+
+    {ok, Results} = couch_db:update_docs(Db, [LocalDoc],
+        [], replicated_changes),
+    ok = couch_db:close(Db),
+    ?assertEqual([], Results),
+
+    {ok, Db2} = couch_db:open_int(DbName, []),
+    Result2 = couch_db:open_doc_int(Db2, ?LOCAL_DOC_ID, []),
+    ok = couch_db:close(Db2),
+    ?assertEqual({not_found, missing}, Result2).
+
+
 spawn_client(DbName, Doc) ->
     spawn(fun() ->
         {ok, Db} = couch_db:open_int(DbName, []),


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
[email protected]


With regards,
Apache Git Services

Reply via email to