blob: 8f93d402edc154e4be5a3e6238a28282cbcad3c1 [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(couch_httpd_view).
-include("couch_db.hrl").
-export([handle_view_req/3,handle_temp_view_req/2]).
-export([get_stale_type/1, get_reduce_type/1, parse_view_params/3]).
-export([make_view_fold_fun/7, finish_view_fold/4, finish_view_fold/5, view_row_obj/4]).
-export([view_group_etag/2, view_group_etag/3, make_reduce_fold_funs/6]).
-export([design_doc_view/5, parse_bool_param/1, doc_member/3]).
-export([make_key_options/1, load_view/4]).
-import(couch_httpd,
[send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,send_chunk/2,
start_json_response/2, start_json_response/3, end_json_response/1,
send_chunked_error/2]).
-import(couch_db,[get_update_seq/1]).
design_doc_view(Req, Db, DName, ViewName, Keys) ->
DesignId = <<"_design/", DName/binary>>,
Stale = get_stale_type(Req),
Reduce = get_reduce_type(Req),
Result = case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of
{ok, View, Group} ->
QueryArgs = parse_view_params(Req, Keys, map),
output_map_view(Req, View, Group, Db, QueryArgs, Keys);
{not_found, Reason} ->
case couch_view:get_reduce_view(Db, DesignId, ViewName, Stale) of
{ok, ReduceView, Group} ->
case Reduce of
false ->
QueryArgs = parse_view_params(Req, Keys, red_map),
MapView = couch_view:extract_map_view(ReduceView),
output_map_view(Req, MapView, Group, Db, QueryArgs, Keys);
_ ->
QueryArgs = parse_view_params(Req, Keys, reduce),
output_reduce_view(Req, Db, ReduceView, Group, QueryArgs, Keys)
end;
_ ->
throw({not_found, Reason})
end
end,
couch_stats_collector:increment({httpd, view_reads}),
Result.
handle_view_req(#httpd{method='GET',
path_parts=[_, _, DName, _, ViewName]}=Req, Db, _DDoc) ->
design_doc_view(Req, Db, DName, ViewName, nil);
handle_view_req(#httpd{method='POST',
path_parts=[_, _, DName, _, ViewName]}=Req, Db, _DDoc) ->
couch_httpd:validate_ctype(Req, "application/json"),
{Fields} = couch_httpd:json_body_obj(Req),
case couch_util:get_value(<<"keys">>, Fields, nil) of
nil ->
Fmt = "POST to view ~p/~p in database ~p with no keys member.",
?LOG_DEBUG(Fmt, [DName, ViewName, Db]),
design_doc_view(Req, Db, DName, ViewName, nil);
Keys when is_list(Keys) ->
design_doc_view(Req, Db, DName, ViewName, Keys);
_ ->
throw({bad_request, "`keys` member must be a array."})
end;
handle_view_req(Req, _Db, _DDoc) ->
send_method_not_allowed(Req, "GET,POST,HEAD").
handle_temp_view_req(#httpd{method='POST'}=Req, Db) ->
couch_httpd:validate_ctype(Req, "application/json"),
ok = couch_db:check_is_admin(Db),
couch_stats_collector:increment({httpd, temporary_view_reads}),
{Props} = couch_httpd:json_body_obj(Req),
Language = couch_util:get_value(<<"language">>, Props, <<"javascript">>),
{DesignOptions} = couch_util:get_value(<<"options">>, Props, {[]}),
MapSrc = couch_util:get_value(<<"map">>, Props),
Keys = couch_util:get_value(<<"keys">>, Props, nil),
Reduce = get_reduce_type(Req),
case couch_util:get_value(<<"reduce">>, Props, null) of
null ->
QueryArgs = parse_view_params(Req, Keys, map),
{ok, View, Group} = couch_view:get_temp_map_view(Db, Language,
DesignOptions, MapSrc),
output_map_view(Req, View, Group, Db, QueryArgs, Keys);
_ when Reduce =:= false ->
QueryArgs = parse_view_params(Req, Keys, red_map),
{ok, View, Group} = couch_view:get_temp_map_view(Db, Language,
DesignOptions, MapSrc),
output_map_view(Req, View, Group, Db, QueryArgs, Keys);
RedSrc ->
QueryArgs = parse_view_params(Req, Keys, reduce),
{ok, View, Group} = couch_view:get_temp_reduce_view(Db, Language,
DesignOptions, MapSrc, RedSrc),
output_reduce_view(Req, Db, View, Group, QueryArgs, Keys)
end;
handle_temp_view_req(Req, _Db) ->
send_method_not_allowed(Req, "POST").
output_map_view(Req, View, Group, Db, QueryArgs, nil) ->
#view_query_args{
limit = Limit,
skip = SkipCount
} = QueryArgs,
CurrentEtag = view_group_etag(Group, Db),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
{ok, RowCount} = couch_view:get_row_count(View),
FoldlFun = make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, Group#group.current_seq, RowCount, #view_fold_helper_funs{reduce_count=fun couch_view:reduce_to_count/1}),
FoldAccInit = {Limit, SkipCount, undefined, []},
{ok, LastReduce, FoldResult} = couch_view:fold(View,
FoldlFun, FoldAccInit, make_key_options(QueryArgs)),
finish_view_fold(Req, RowCount,
couch_view:reduce_to_count(LastReduce), FoldResult)
end);
output_map_view(Req, View, Group, Db, QueryArgs, Keys) ->
#view_query_args{
limit = Limit,
skip = SkipCount
} = QueryArgs,
CurrentEtag = view_group_etag(Group, Db, Keys),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
{ok, RowCount} = couch_view:get_row_count(View),
FoldAccInit = {Limit, SkipCount, undefined, []},
{LastReduce, FoldResult} = lists:foldl(fun(Key, {_, FoldAcc}) ->
FoldlFun = make_view_fold_fun(Req, QueryArgs#view_query_args{},
CurrentEtag, Db, Group#group.current_seq, RowCount,
#view_fold_helper_funs{
reduce_count = fun couch_view:reduce_to_count/1
}),
{ok, LastReduce, FoldResult} = couch_view:fold(View, FoldlFun,
FoldAcc, make_key_options(
QueryArgs#view_query_args{start_key=Key, end_key=Key})),
{LastReduce, FoldResult}
end, {{[],[]}, FoldAccInit}, Keys),
finish_view_fold(Req, RowCount, couch_view:reduce_to_count(LastReduce),
FoldResult, [{update_seq,Group#group.current_seq}])
end).
output_reduce_view(Req, Db, View, Group, QueryArgs, nil) ->
#view_query_args{
limit = Limit,
skip = Skip,
group_level = GroupLevel
} = QueryArgs,
CurrentEtag = view_group_etag(Group, Db),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
{ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel,
QueryArgs, CurrentEtag, Group#group.current_seq,
#reduce_fold_helper_funs{}),
FoldAccInit = {Limit, Skip, undefined, []},
{ok, {_, _, Resp, _}} = couch_view:fold_reduce(View,
RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} |
make_key_options(QueryArgs)]),
finish_reduce_fold(Req, Resp)
end);
output_reduce_view(Req, Db, View, Group, QueryArgs, Keys) ->
#view_query_args{
limit = Limit,
skip = Skip,
group_level = GroupLevel
} = QueryArgs,
CurrentEtag = view_group_etag(Group, Db, Keys),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
{ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel,
QueryArgs, CurrentEtag, Group#group.current_seq,
#reduce_fold_helper_funs{}),
{Resp, _RedAcc3} = lists:foldl(
fun(Key, {Resp, RedAcc}) ->
% run the reduce once for each key in keys, with limit etc
% reapplied for each key
FoldAccInit = {Limit, Skip, Resp, RedAcc},
{_, {_, _, Resp2, RedAcc2}} = couch_view:fold_reduce(View,
RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} |
make_key_options(QueryArgs#view_query_args{
start_key=Key, end_key=Key})]),
% Switch to comma
{Resp2, RedAcc2}
end,
{undefined, []}, Keys), % Start with no comma
finish_reduce_fold(Req, Resp, [{update_seq,Group#group.current_seq}])
end).
reverse_key_default(?MIN_STR) -> ?MAX_STR;
reverse_key_default(?MAX_STR) -> ?MIN_STR;
reverse_key_default(Key) -> Key.
get_stale_type(Req) ->
list_to_existing_atom(couch_httpd:qs_value(Req, "stale", "nil")).
get_reduce_type(Req) ->
list_to_existing_atom(couch_httpd:qs_value(Req, "reduce", "true")).
load_view(Req, Db, {ViewDesignId, ViewName}, Keys) ->
Stale = get_stale_type(Req),
Reduce = get_reduce_type(Req),
case couch_view:get_map_view(Db, ViewDesignId, ViewName, Stale) of
{ok, View, Group} ->
QueryArgs = parse_view_params(Req, Keys, map),
{map, View, Group, QueryArgs};
{not_found, _Reason} ->
case couch_view:get_reduce_view(Db, ViewDesignId, ViewName, Stale) of
{ok, ReduceView, Group} ->
case Reduce of
false ->
QueryArgs = parse_view_params(Req, Keys, map_red),
MapView = couch_view:extract_map_view(ReduceView),
{map, MapView, Group, QueryArgs};
_ ->
QueryArgs = parse_view_params(Req, Keys, reduce),
{reduce, ReduceView, Group, QueryArgs}
end;
{not_found, Reason} ->
throw({not_found, Reason})
end
end.
% query_parse_error could be removed
% we wouldn't need to pass the view type, it'd just parse params.
% I'm not sure what to do about the error handling, but
% it might simplify things to have a parse_view_params function
% that doesn't throw().
parse_view_params(Req, Keys, ViewType) ->
QueryList = couch_httpd:qs(Req),
QueryParams =
lists:foldl(fun({K, V}, Acc) ->
parse_view_param(K, V) ++ Acc
end, [], QueryList),
IsMultiGet = (Keys =/= nil),
Args = #view_query_args{
view_type=ViewType,
multi_get=IsMultiGet
},
QueryArgs = lists:foldl(fun({K, V}, Args2) ->
validate_view_query(K, V, Args2)
end, Args, lists:reverse(QueryParams)), % Reverse to match QS order.
GroupLevel = QueryArgs#view_query_args.group_level,
case {ViewType, GroupLevel, IsMultiGet} of
{reduce, exact, true} ->
QueryArgs;
{reduce, _, false} ->
QueryArgs;
{reduce, _, _} ->
% we can simplify code if we just drop this error message.
Msg = <<"Multi-key fetchs for reduce "
"view must include `group=true`">>,
throw({query_parse_error, Msg});
_ ->
QueryArgs
end,
QueryArgs.
parse_view_param("", _) ->
[];
parse_view_param("key", Value) ->
JsonKey = ?JSON_DECODE(Value),
[{start_key, JsonKey}, {end_key, JsonKey}];
parse_view_param("startkey_docid", Value) ->
[{start_docid, ?l2b(Value)}];
parse_view_param("endkey_docid", Value) ->
[{end_docid, ?l2b(Value)}];
parse_view_param("startkey", Value) ->
[{start_key, ?JSON_DECODE(Value)}];
parse_view_param("endkey", Value) ->
[{end_key, ?JSON_DECODE(Value)}];
parse_view_param("limit", Value) ->
[{limit, parse_positive_int_param(Value)}];
parse_view_param("count", _Value) ->
throw({query_parse_error, <<"Query parameter 'count' is now 'limit'.">>});
parse_view_param("stale", "ok") ->
[{stale, ok}];
parse_view_param("stale", _Value) ->
throw({query_parse_error, <<"stale only available as stale=ok">>});
parse_view_param("update", _Value) ->
throw({query_parse_error, <<"update=false is now stale=ok">>});
parse_view_param("descending", Value) ->
[{descending, parse_bool_param(Value)}];
parse_view_param("skip", Value) ->
[{skip, parse_int_param(Value)}];
parse_view_param("group", Value) ->
case parse_bool_param(Value) of
true -> [{group_level, exact}];
false -> [{group_level, 0}]
end;
parse_view_param("group_level", Value) ->
[{group_level, parse_positive_int_param(Value)}];
parse_view_param("inclusive_end", Value) ->
[{inclusive_end, parse_bool_param(Value)}];
parse_view_param("reduce", Value) ->
[{reduce, parse_bool_param(Value)}];
parse_view_param("include_docs", Value) ->
[{include_docs, parse_bool_param(Value)}];
parse_view_param("conflicts", Value) ->
[{conflicts, parse_bool_param(Value)}];
parse_view_param("list", Value) ->
[{list, ?l2b(Value)}];
parse_view_param("callback", _) ->
[]; % Verified in the JSON response functions
parse_view_param(Key, Value) ->
[{extra, {Key, Value}}].
validate_view_query(start_key, Value, Args) ->
case Args#view_query_args.multi_get of
true ->
Msg = <<"Query parameter `start_key` is "
"not compatible with multi-get">>,
throw({query_parse_error, Msg});
_ ->
Args#view_query_args{start_key=Value}
end;
validate_view_query(start_docid, Value, Args) ->
Args#view_query_args{start_docid=Value};
validate_view_query(end_key, Value, Args) ->
case Args#view_query_args.multi_get of
true->
Msg = <<"Query parameter `end_key` is "
"not compatible with multi-get">>,
throw({query_parse_error, Msg});
_ ->
Args#view_query_args{end_key=Value}
end;
validate_view_query(end_docid, Value, Args) ->
Args#view_query_args{end_docid=Value};
validate_view_query(limit, Value, Args) ->
Args#view_query_args{limit=Value};
validate_view_query(list, Value, Args) ->
Args#view_query_args{list=Value};
validate_view_query(stale, _, Args) ->
Args;
validate_view_query(descending, true, Args) ->
case Args#view_query_args.direction of
rev -> Args; % Already reversed
fwd ->
Args#view_query_args{
direction = rev,
start_docid =
reverse_key_default(Args#view_query_args.start_docid),
end_docid =
reverse_key_default(Args#view_query_args.end_docid)
}
end;
validate_view_query(descending, false, Args) ->
Args; % Ignore default condition
validate_view_query(skip, Value, Args) ->
Args#view_query_args{skip=Value};
validate_view_query(group_level, Value, Args) ->
case Args#view_query_args.view_type of
reduce ->
Args#view_query_args{group_level=Value};
_ ->
Msg = <<"Invalid URL parameter 'group' or "
" 'group_level' for non-reduce view.">>,
throw({query_parse_error, Msg})
end;
validate_view_query(inclusive_end, Value, Args) ->
Args#view_query_args{inclusive_end=Value};
validate_view_query(reduce, false, Args) ->
Args;
validate_view_query(reduce, _, Args) ->
case Args#view_query_args.view_type of
map ->
Msg = <<"Invalid URL parameter `reduce` for map view.">>,
throw({query_parse_error, Msg});
_ ->
Args
end;
validate_view_query(include_docs, true, Args) ->
case Args#view_query_args.view_type of
reduce ->
Msg = <<"Query parameter `include_docs` "
"is invalid for reduce views.">>,
throw({query_parse_error, Msg});
_ ->
Args#view_query_args{include_docs=true}
end;
% Use the view_query_args record's default value
validate_view_query(include_docs, _Value, Args) ->
Args;
validate_view_query(conflicts, true, Args) ->
case Args#view_query_args.view_type of
reduce ->
Msg = <<"Query parameter `conflicts` "
"is invalid for reduce views.">>,
throw({query_parse_error, Msg});
_ ->
Args#view_query_args{conflicts = true}
end;
validate_view_query(extra, _Value, Args) ->
Args.
make_view_fold_fun(Req, QueryArgs, Etag, Db, UpdateSeq, TotalViewCount, HelperFuns) ->
#view_fold_helper_funs{
start_response = StartRespFun,
send_row = SendRowFun,
reduce_count = ReduceCountFun
} = apply_default_helper_funs(HelperFuns),
#view_query_args{
include_docs = IncludeDocs,
conflicts = Conflicts
} = QueryArgs,
fun({{Key, DocId}, Value}, OffsetReds,
{AccLimit, AccSkip, Resp, RowFunAcc}) ->
case {AccLimit, AccSkip, Resp} of
{0, _, _} ->
% we've done "limit" rows, stop foldling
{stop, {0, 0, Resp, RowFunAcc}};
{_, AccSkip, _} when AccSkip > 0 ->
% just keep skipping
{ok, {AccLimit, AccSkip - 1, Resp, RowFunAcc}};
{_, _, undefined} ->
% rendering the first row, first we start the response
Offset = ReduceCountFun(OffsetReds),
{ok, Resp2, RowFunAcc0} = StartRespFun(Req, Etag,
TotalViewCount, Offset, RowFunAcc, UpdateSeq),
{Go, RowFunAcc2} = SendRowFun(Resp2, Db, {{Key, DocId}, Value},
IncludeDocs, Conflicts, RowFunAcc0),
{Go, {AccLimit - 1, 0, Resp2, RowFunAcc2}};
{AccLimit, _, Resp} when (AccLimit > 0) ->
% rendering all other rows
{Go, RowFunAcc2} = SendRowFun(Resp, Db, {{Key, DocId}, Value},
IncludeDocs, Conflicts, RowFunAcc),
{Go, {AccLimit - 1, 0, Resp, RowFunAcc2}}
end
end.
make_reduce_fold_funs(Req, GroupLevel, _QueryArgs, Etag, UpdateSeq, HelperFuns) ->
#reduce_fold_helper_funs{
start_response = StartRespFun,
send_row = SendRowFun
} = apply_default_helper_funs(HelperFuns),
GroupRowsFun =
fun({_Key1,_}, {_Key2,_}) when GroupLevel == 0 ->
true;
({Key1,_}, {Key2,_})
when is_integer(GroupLevel) and is_list(Key1) and is_list(Key2) ->
lists:sublist(Key1, GroupLevel) == lists:sublist(Key2, GroupLevel);
({Key1,_}, {Key2,_}) ->
Key1 == Key2
end,
RespFun = fun
(_Key, _Red, {AccLimit, AccSkip, Resp, RowAcc}) when AccSkip > 0 ->
% keep skipping
{ok, {AccLimit, AccSkip - 1, Resp, RowAcc}};
(_Key, _Red, {0, _AccSkip, Resp, RowAcc}) ->
% we've exhausted limit rows, stop
{stop, {0, _AccSkip, Resp, RowAcc}};
(_Key, Red, {AccLimit, 0, undefined, RowAcc0}) when GroupLevel == 0 ->
% we haven't started responding yet and group=false
{ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0, UpdateSeq),
{Go, RowAcc2} = SendRowFun(Resp2, {null, Red}, RowAcc),
{Go, {AccLimit - 1, 0, Resp2, RowAcc2}};
(_Key, Red, {AccLimit, 0, Resp, RowAcc}) when GroupLevel == 0 ->
% group=false but we've already started the response
{Go, RowAcc2} = SendRowFun(Resp, {null, Red}, RowAcc),
{Go, {AccLimit - 1, 0, Resp, RowAcc2}};
(Key, Red, {AccLimit, 0, undefined, RowAcc0})
when is_integer(GroupLevel), is_list(Key) ->
% group_level and we haven't responded yet
{ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0, UpdateSeq),
{Go, RowAcc2} = SendRowFun(Resp2,
{lists:sublist(Key, GroupLevel), Red}, RowAcc),
{Go, {AccLimit - 1, 0, Resp2, RowAcc2}};
(Key, Red, {AccLimit, 0, Resp, RowAcc})
when is_integer(GroupLevel), is_list(Key) ->
% group_level and we've already started the response
{Go, RowAcc2} = SendRowFun(Resp,
{lists:sublist(Key, GroupLevel), Red}, RowAcc),
{Go, {AccLimit - 1, 0, Resp, RowAcc2}};
(Key, Red, {AccLimit, 0, undefined, RowAcc0}) ->
% group=true and we haven't responded yet
{ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0, UpdateSeq),
{Go, RowAcc2} = SendRowFun(Resp2, {Key, Red}, RowAcc),
{Go, {AccLimit - 1, 0, Resp2, RowAcc2}};
(Key, Red, {AccLimit, 0, Resp, RowAcc}) ->
% group=true and we've already started the response
{Go, RowAcc2} = SendRowFun(Resp, {Key, Red}, RowAcc),
{Go, {AccLimit - 1, 0, Resp, RowAcc2}}
end,
{ok, GroupRowsFun, RespFun}.
apply_default_helper_funs(
#view_fold_helper_funs{
start_response = StartResp,
send_row = SendRow
}=Helpers) ->
StartResp2 = case StartResp of
undefined -> fun json_view_start_resp/6;
_ -> StartResp
end,
SendRow2 = case SendRow of
undefined -> fun send_json_view_row/6;
_ -> SendRow
end,
Helpers#view_fold_helper_funs{
start_response = StartResp2,
send_row = SendRow2
};
apply_default_helper_funs(
#reduce_fold_helper_funs{
start_response = StartResp,
send_row = SendRow
}=Helpers) ->
StartResp2 = case StartResp of
undefined -> fun json_reduce_start_resp/4;
_ -> StartResp
end,
SendRow2 = case SendRow of
undefined -> fun send_json_reduce_row/3;
_ -> SendRow
end,
Helpers#reduce_fold_helper_funs{
start_response = StartResp2,
send_row = SendRow2
}.
make_key_options(#view_query_args{direction = Dir}=QueryArgs) ->
[{dir,Dir} | make_start_key_option(QueryArgs) ++
make_end_key_option(QueryArgs)].
make_start_key_option(
#view_query_args{
start_key = StartKey,
start_docid = StartDocId}) ->
if StartKey == undefined ->
[];
true ->
[{start_key, {StartKey, StartDocId}}]
end.
make_end_key_option(#view_query_args{end_key = undefined}) ->
[];
make_end_key_option(
#view_query_args{end_key = EndKey,
end_docid = EndDocId,
inclusive_end = true}) ->
[{end_key, {EndKey, EndDocId}}];
make_end_key_option(
#view_query_args{
end_key = EndKey,
end_docid = EndDocId,
inclusive_end = false}) ->
[{end_key_gt, {EndKey,reverse_key_default(EndDocId)}}].
json_view_start_resp(Req, Etag, TotalViewCount, Offset, _Acc, UpdateSeq) ->
{ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]),
BeginBody = case couch_httpd:qs_value(Req, "update_seq") of
"true" ->
io_lib:format(
"{\"total_rows\":~w,\"update_seq\":~w,"
"\"offset\":~w,\"rows\":[\r\n",
[TotalViewCount, UpdateSeq, Offset]);
_Else ->
io_lib:format(
"{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n",
[TotalViewCount, Offset])
end,
{ok, Resp, BeginBody}.
send_json_view_row(Resp, Db, Kv, IncludeDocs, Conflicts, RowFront) ->
JsonObj = view_row_obj(Db, Kv, IncludeDocs, Conflicts),
send_chunk(Resp, RowFront ++ ?JSON_ENCODE(JsonObj)),
{ok, ",\r\n"}.
json_reduce_start_resp(Req, Etag, _Acc0, UpdateSeq) ->
{ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]),
case couch_httpd:qs_value(Req, "update_seq") of
"true" ->
{ok, Resp, io_lib:format("{\"update_seq\":~w,\"rows\":[\r\n",[UpdateSeq])};
_Else ->
{ok, Resp, "{\"rows\":[\r\n"}
end.
send_json_reduce_row(Resp, {Key, Value}, RowFront) ->
send_chunk(Resp, RowFront ++ ?JSON_ENCODE({[{key, Key}, {value, Value}]})),
{ok, ",\r\n"}.
view_group_etag(Group, Db) ->
view_group_etag(Group, Db, nil).
view_group_etag(#group{sig=Sig,current_seq=CurrentSeq}, _Db, Extra) ->
% ?LOG_ERROR("Group ~p",[Group]),
% This is not as granular as it could be.
% If there are updates to the db that do not effect the view index,
% they will change the Etag. For more granular Etags we'd need to keep
% track of the last Db seq that caused an index change.
couch_httpd:make_etag({Sig, CurrentSeq, Extra}).
% the view row has an error
view_row_obj(_Db, {{Key, error}, Value}, _IncludeDocs, _Conflicts) ->
{[{key, Key}, {error, Value}]};
% include docs in the view output
view_row_obj(Db, {{Key, DocId}, {Props}}, true, Conflicts) ->
Rev = case couch_util:get_value(<<"_rev">>, Props) of
undefined ->
nil;
Rev0 ->
couch_doc:parse_rev(Rev0)
end,
IncludeId = couch_util:get_value(<<"_id">>, Props, DocId),
view_row_with_doc(Db, {{Key, DocId}, {Props}}, {IncludeId, Rev}, Conflicts);
view_row_obj(Db, {{Key, DocId}, Value}, true, Conflicts) ->
view_row_with_doc(Db, {{Key, DocId}, Value}, {DocId, nil}, Conflicts);
% the normal case for rendering a view row
view_row_obj(_Db, {{Key, DocId}, Value}, _IncludeDocs, _Conflicts) ->
{[{id, DocId}, {key, Key}, {value, Value}]}.
view_row_with_doc(Db, {{Key, DocId}, Value}, IdRev, Conflicts) ->
{[{id, DocId}, {key, Key}, {value, Value}] ++
doc_member(Db, IdRev, if Conflicts -> [conflicts]; true -> [] end)}.
doc_member(Db, #doc_info{id = Id, revs = [#rev_info{rev = Rev} | _]} = Info,
Options) ->
?LOG_DEBUG("Include Doc: ~p ~p", [Id, Rev]),
case couch_db:open_doc(Db, Info, [deleted | Options]) of
{ok, Doc} ->
[{doc, couch_doc:to_json_obj(Doc, [])}];
_ ->
[{doc, null}]
end;
doc_member(Db, {DocId, Rev}, Options) ->
?LOG_DEBUG("Include Doc: ~p ~p", [DocId, Rev]),
case (catch couch_httpd_db:couch_doc_open(Db, DocId, Rev, Options)) of
#doc{} = Doc ->
JsonDoc = couch_doc:to_json_obj(Doc, []),
[{doc, JsonDoc}];
_Else ->
[{doc, null}]
end.
finish_view_fold(Req, TotalRows, Offset, FoldResult) ->
finish_view_fold(Req, TotalRows, Offset, FoldResult, []).
finish_view_fold(Req, TotalRows, Offset, FoldResult, Fields) ->
case FoldResult of
{_, _, undefined, _} ->
% nothing found in the view or keys, nothing has been returned
% send empty view
send_json(Req, 200, {[
{total_rows, TotalRows},
{offset, Offset},
{rows, []}
] ++ Fields});
{_, _, Resp, _} ->
% end the view
send_chunk(Resp, "\r\n]}"),
end_json_response(Resp)
end.
finish_reduce_fold(Req, Resp) ->
finish_reduce_fold(Req, Resp, []).
finish_reduce_fold(Req, Resp, Fields) ->
case Resp of
undefined ->
send_json(Req, 200, {[
{rows, []}
] ++ Fields});
Resp ->
send_chunk(Resp, "\r\n]}"),
end_json_response(Resp)
end.
parse_bool_param(Val) ->
case string:to_lower(Val) of
"true" -> true;
"false" -> false;
_ ->
Msg = io_lib:format("Invalid boolean parameter: ~p", [Val]),
throw({query_parse_error, ?l2b(Msg)})
end.
parse_int_param(Val) ->
case (catch list_to_integer(Val)) of
IntVal when is_integer(IntVal) ->
IntVal;
_ ->
Msg = io_lib:format("Invalid value for integer parameter: ~p", [Val]),
throw({query_parse_error, ?l2b(Msg)})
end.
parse_positive_int_param(Val) ->
case parse_int_param(Val) of
IntVal when IntVal >= 0 ->
IntVal;
_ ->
Fmt = "Invalid value for positive integer parameter: ~p",
Msg = io_lib:format(Fmt, [Val]),
throw({query_parse_error, ?l2b(Msg)})
end.