summaryrefslogtreecommitdiff
path: root/src/couchdb/couch_doc.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/couchdb/couch_doc.erl')
-rw-r--r--src/couchdb/couch_doc.erl185
1 files changed, 115 insertions, 70 deletions
diff --git a/src/couchdb/couch_doc.erl b/src/couchdb/couch_doc.erl
index 9860ac0c..fc817d56 100644
--- a/src/couchdb/couch_doc.erl
+++ b/src/couchdb/couch_doc.erl
@@ -12,41 +12,53 @@
-module(couch_doc).
--export([to_doc_info/1,to_doc_info_path/1]).
+-export([to_doc_info/1,to_doc_info_path/1,parse_rev/1,parse_revs/1,rev_to_str/1,rev_to_strs/1]).
-export([bin_foldl/3,bin_size/1,bin_to_binary/1,get_validate_doc_fun/1]).
-export([from_json_obj/1,to_json_obj/2,has_stubs/1, merge_stubs/2]).
-include("couch_db.hrl").
% helpers used by to_json_obj
-to_json_rev([]) ->
+to_json_rev(0, []) ->
[];
-to_json_rev(Revs) ->
- [{<<"_rev">>, lists:nth(1, Revs)}].
+to_json_rev(Start, [FirstRevId|_]) ->
+ [{<<"_rev">>, ?l2b([integer_to_list(Start),"-",FirstRevId])}].
to_json_body(true, _Body) ->
[{<<"_deleted">>, true}];
to_json_body(false, {Body}) ->
Body.
-to_json_revs(Options, Revs) ->
+to_json_revisions(Options, Start, RevIds) ->
case lists:member(revs, Options) of
false -> [];
true ->
- [{<<"_revs">>, Revs}]
+ [{<<"_revisions">>, {[{<<"start">>, Start},
+ {<<"ids">>, RevIds}]}}]
end.
-to_json_revs_info(Meta) ->
+rev_to_str({Pos, RevId}) ->
+ ?l2b([integer_to_list(Pos),"-",RevId]).
+
+rev_to_strs([]) ->
+ [];
+rev_to_strs([{Pos, RevId}| Rest]) ->
+ [rev_to_str({Pos, RevId}) | rev_to_strs(Rest)].
+
+to_json_meta(Meta) ->
lists:map(
- fun({revs_info, RevsInfo}) ->
- JsonRevsInfo =
- [{[{rev, Rev}, {status, list_to_binary(atom_to_list(Status))}]} ||
- {Rev, Status} <- RevsInfo],
+ fun({revs_info, Start, RevsInfo}) ->
+ {JsonRevsInfo, _Pos} = lists:mapfoldl(
+ fun({RevId, Status}, PosAcc) ->
+ JsonObj = {[{<<"rev">>, rev_to_str({PosAcc, RevId})},
+ {<<"status">>, ?l2b(atom_to_list(Status))}]},
+ {JsonObj, PosAcc - 1}
+ end, Start, RevsInfo),
{<<"_revs_info">>, JsonRevsInfo};
({conflicts, Conflicts}) ->
- {<<"_conflicts">>, Conflicts};
- ({deleted_conflicts, Conflicts}) ->
- {<<"_deleted_conflicts">>, Conflicts}
+ {<<"_conflicts">>, rev_to_strs(Conflicts)};
+ ({deleted_conflicts, DConflicts}) ->
+ {<<"_deleted_conflicts">>, rev_to_strs(DConflicts)}
end, Meta).
to_json_attachment_stubs(Attachments) ->
@@ -98,17 +110,62 @@ to_json_attachments(Attachments, Options) ->
to_json_attachment_stubs(Attachments)
end.
-to_json_obj(#doc{id=Id,deleted=Del,body=Body,revs=Revs,meta=Meta}=Doc,Options)->
+to_json_obj(#doc{id=Id,deleted=Del,body=Body,revs={Start, RevIds},
+ meta=Meta}=Doc,Options)->
{[{<<"_id">>, Id}]
- ++ to_json_rev(Revs)
+ ++ to_json_rev(Start, RevIds)
++ to_json_body(Del, Body)
- ++ to_json_revs(Options, Revs)
- ++ to_json_revs_info(Meta)
+ ++ to_json_revisions(Options, Start, RevIds)
+ ++ to_json_meta(Meta)
++ to_json_attachments(Doc#doc.attachments, Options)
}.
from_json_obj({Props}) ->
- {JsonBins} = proplists:get_value(<<"_attachments">>, Props, {[]}),
+ transfer_fields(Props, #doc{body=[]});
+
+from_json_obj(_Other) ->
+ throw({bad_request, "Document must be a JSON object"}).
+
+parse_rev(Rev) when is_binary(Rev) ->
+ parse_rev(?b2l(Rev));
+parse_rev(Rev) ->
+ {Pos, [$- | RevId]} = lists:splitwith(fun($-) -> false; (_) -> true end, Rev),
+ {list_to_integer(Pos), ?l2b(RevId)}.
+
+parse_revs([]) ->
+ [];
+parse_revs([Rev | Rest]) ->
+ [parse_rev(Rev) | parse_revs(Rest)].
+
+
+transfer_fields([], #doc{body=Fields}=Doc) ->
+ % convert fields back to json object
+ Doc#doc{body={lists:reverse(Fields)}};
+
+transfer_fields([{<<"_id">>, Id} | Rest], Doc) when is_binary(Id) ->
+ case Id of
+ <<"_design/", _/binary>> -> ok;
+ <<"_local/", _/binary>> -> ok;
+ <<"_", _/binary>> ->
+ throw({bad_request, <<"Only reserved document ids may start with underscore.">>});
+ _Else -> ok
+ end,
+ transfer_fields(Rest, Doc#doc{id=Id});
+
+transfer_fields([{<<"_id">>, Id} | _Rest], _Doc) ->
+ ?LOG_DEBUG("Document id is not a string: ~p", [Id]),
+ throw({bad_request, <<"Document id must be a string">>});
+
+transfer_fields([{<<"_rev">>, Rev} | Rest], #doc{revs={0, []}}=Doc) ->
+ {Pos, RevId} = parse_rev(Rev),
+ transfer_fields(Rest,
+ Doc#doc{revs={Pos, [RevId]}});
+
+transfer_fields([{<<"_rev">>, _Rev} | Rest], Doc) ->
+ % we already got the rev from the _revisions
+ transfer_fields(Rest,Doc);
+
+transfer_fields([{<<"_attachments">>, {JsonBins}} | Rest], Doc) ->
Bins = lists:flatmap(fun({Name, {BinProps}}) ->
case proplists:get_value(<<"stub">>, BinProps) of
true ->
@@ -122,51 +179,40 @@ from_json_obj({Props}) ->
[{Name, {Type, couch_util:decodeBase64(Value)}}]
end
end, JsonBins),
- AllowedSpecialMembers = [<<"id">>, <<"revs">>, <<"rev">>, <<"attachments">>, <<"revs_info">>,
- <<"conflicts">>, <<"deleted_conflicts">>, <<"deleted">>],
- % collect all the doc-members that start with "_"
- % if any aren't in the AllowedSpecialMembers list
- % then throw a invalid_doc error
- [case lists:member(Name, AllowedSpecialMembers) of
- true ->
- ok;
- false ->
- throw({invalid_doc, io_lib:format("Bad special document member: _~s", [Name])})
- end
- || {<<$_,Name/binary>>, _Value} <- Props],
- Revs =
- case proplists:get_value(<<"_revs">>, Props, []) of
- [] ->
- case proplists:get_value(<<"_rev">>, Props) of
- undefined -> [];
- Rev -> [Rev]
- end;
- Revs0 ->
- Revs0
- end,
- case proplists:get_value(<<"_id">>, Props, <<>>) of
- <<"_design/", _/binary>> = Id -> ok;
- <<"_local/", _/binary>> = Id -> ok;
- <<"_", _/binary>> = Id ->
- throw({invalid_doc, "Document Ids must not start with underscore."});
- Id when is_binary(Id) -> ok;
- Id ->
- ?LOG_DEBUG("Document id is not a string: ~p", [Id]),
- throw({invalid_doc, "Document id is not a string"})
+ transfer_fields(Rest, Doc#doc{attachments=Bins});
+
+transfer_fields([{<<"_revisions">>, {Props}} | Rest], Doc) ->
+ RevIds = proplists:get_value(<<"ids">>, Props),
+ Start = proplists:get_value(<<"start">>, Props),
+ if not is_integer(Start) ->
+ throw({doc_validation, "_revisions.start isn't an integer."});
+ not is_list(RevIds) ->
+ throw({doc_validation, "_revisions.ids isn't a array."});
+ true ->
+ ok
end,
+ [throw({doc_validation, "RevId isn't a string"}) ||
+ RevId <- RevIds, not is_binary(RevId)],
+ transfer_fields(Rest, Doc#doc{revs={Start, RevIds}});
- % strip out the all props beginning with _
- NewBody = {[{K, V} || {<<First,_/binary>>=K, V} <- Props, First /= $_]},
- #doc{
- id = Id,
- revs = Revs,
- deleted = proplists:get_value(<<"_deleted">>, Props, false),
- body = NewBody,
- attachments = Bins
- };
+transfer_fields([{<<"_deleted">>, B} | Rest], Doc) when (B==true) or (B==false) ->
+ transfer_fields(Rest, Doc#doc{deleted=B});
-from_json_obj(_Other) ->
- throw({invalid_doc, "Document must be a JSON object"}).
+% ignored fields
+transfer_fields([{<<"_revs_info">>, _} | Rest], Doc) ->
+ transfer_fields(Rest, Doc);
+transfer_fields([{<<"_conflicts">>, _} | Rest], Doc) ->
+ transfer_fields(Rest, Doc);
+transfer_fields([{<<"_deleted_conflicts">>, _} | Rest], Doc) ->
+ transfer_fields(Rest, Doc);
+
+% unknown special field
+transfer_fields([{<<"_",Name/binary>>, Start} | _], _) when is_integer(Start) ->
+ throw({doc_validation,
+ ?l2b(io_lib:format("Bad special document member: _~s", [Name]))});
+
+transfer_fields([Field | Rest], #doc{body=Fields}=Doc) ->
+ transfer_fields(Rest, Doc#doc{body=[Field|Fields]}).
to_doc_info(FullDocInfo) ->
{DocInfo, _Path} = to_doc_info_path(FullDocInfo),
@@ -175,27 +221,26 @@ to_doc_info(FullDocInfo) ->
to_doc_info_path(#full_doc_info{id=Id,update_seq=Seq,rev_tree=Tree}) ->
LeafRevs = couch_key_tree:get_all_leafs(Tree),
SortedLeafRevs =
- lists:sort(fun({RevIdA, {IsDeletedA, _}, PathA}, {RevIdB, {IsDeletedB, _}, PathB}) ->
+ lists:sort(fun({{IsDeletedA, _}, {StartA, [RevIdA|_]}}, {{IsDeletedB, _}, {StartB, [RevIdB|_]}}) ->
% sort descending by {not deleted, then Depth, then RevisionId}
- A = {not IsDeletedA, length(PathA), RevIdA},
- B = {not IsDeletedB, length(PathB), RevIdB},
+ A = {not IsDeletedA, StartA, RevIdA},
+ B = {not IsDeletedB, StartB, RevIdB},
A > B
end,
LeafRevs),
- [{RevId, {IsDeleted, SummaryPointer}, Path} | Rest] = SortedLeafRevs,
-
+ [{{IsDeleted, SummaryPointer}, {Start, [RevId|_]}=Path} | Rest] = SortedLeafRevs,
{ConflictRevTuples, DeletedConflictRevTuples} =
- lists:splitwith(fun({_ConflictRevId, {IsDeleted1, _Sp}, _}) ->
+ lists:splitwith(fun({{IsDeleted1, _Sp}, _}) ->
not IsDeleted1
end, Rest),
- ConflictRevs = [RevId1 || {RevId1, _, _} <- ConflictRevTuples],
- DeletedConflictRevs = [RevId2 || {RevId2, _, _} <- DeletedConflictRevTuples],
+ ConflictRevs = [{Start1, RevId1} || {_, {Start1, [RevId1|_]}} <- ConflictRevTuples],
+ DeletedConflictRevs = [{Start1, RevId1} || {_, {Start1, [RevId1|_]}} <- DeletedConflictRevTuples],
DocInfo = #doc_info{
id=Id,
update_seq=Seq,
- rev = RevId,
+ rev = {Start, RevId},
summary_pointer = SummaryPointer,
conflict_revs = ConflictRevs,
deleted_conflict_revs = DeletedConflictRevs,