PageRenderTime 28ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/apps/couch/src/couch_httpd_view.erl

http://github.com/cloudant/bigcouch
Erlang | 777 lines | 668 code | 62 blank | 47 comment | 15 complexity | 8ce56aa8c51c0fc74df9fd29518d6cbf MD5 | raw file
Possible License(s): Apache-2.0
  1. % Licensed under the Apache License, Version 2.0 (the "License"); you may not
  2. % use this file except in compliance with the License. You may obtain a copy of
  3. % the License at
  4. %
  5. % http://www.apache.org/licenses/LICENSE-2.0
  6. %
  7. % Unless required by applicable law or agreed to in writing, software
  8. % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. % License for the specific language governing permissions and limitations under
  11. % the License.
  12. -module(couch_httpd_view).
  13. -include("couch_db.hrl").
  14. -export([handle_view_req/3,handle_temp_view_req/2]).
  15. -export([parse_view_params/4]).
  16. -export([make_view_fold_fun/7, finish_view_fold/4, finish_view_fold/5, view_row_obj/4]).
  17. -export([view_etag/5, make_reduce_fold_funs/6]).
  18. -export([design_doc_view/5, parse_bool_param/1, doc_member/3]).
  19. -export([make_key_options/1, load_view/4]).
  20. -import(couch_httpd,
  21. [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,send_chunk/2,
  22. start_json_response/2, start_json_response/3, end_json_response/1,
  23. send_chunked_error/2]).
  24. -import(couch_db,[get_update_seq/1]).
  25. design_doc_view(Req, Db, DName, ViewName, Keys) ->
  26. DesignId = <<"_design/", DName/binary>>,
  27. Stale = get_stale_type(Req),
  28. Reduce = get_reduce_type(Req),
  29. Result = case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of
  30. {ok, View, Group} ->
  31. QueryArgs = parse_view_params(Req, Keys, map, view_collator(View)),
  32. output_map_view(Req, View, Group, Db, QueryArgs, Keys);
  33. {not_found, Reason} ->
  34. case couch_view:get_reduce_view(Db, DesignId, ViewName, Stale) of
  35. {ok, ReduceView, Group} ->
  36. Collator = view_collator(ReduceView),
  37. case Reduce of
  38. false ->
  39. QueryArgs = parse_view_params(Req, Keys, red_map, Collator),
  40. MapView = couch_view:extract_map_view(ReduceView),
  41. output_map_view(Req, MapView, Group, Db, QueryArgs, Keys);
  42. _ ->
  43. QueryArgs = parse_view_params(Req, Keys, reduce, Collator),
  44. output_reduce_view(Req, Db, ReduceView, Group, QueryArgs, Keys)
  45. end;
  46. _ ->
  47. throw({not_found, Reason})
  48. end
  49. end,
  50. couch_stats_collector:increment({httpd, view_reads}),
  51. Result.
  52. handle_view_req(#httpd{method='GET',
  53. path_parts=[_, _, DName, _, ViewName]}=Req, Db, _DDoc) ->
  54. Keys = couch_httpd:qs_json_value(Req, "keys", nil),
  55. design_doc_view(Req, Db, DName, ViewName, Keys);
  56. handle_view_req(#httpd{method='POST',
  57. path_parts=[_, _, DName, _, ViewName]}=Req, Db, _DDoc) ->
  58. couch_httpd:validate_ctype(Req, "application/json"),
  59. {Fields} = couch_httpd:json_body_obj(Req),
  60. case couch_util:get_value(<<"keys">>, Fields, nil) of
  61. nil ->
  62. Fmt = "POST to view ~p/~p in database ~p with no keys member.",
  63. ?LOG_DEBUG(Fmt, [DName, ViewName, Db]),
  64. design_doc_view(Req, Db, DName, ViewName, nil);
  65. Keys when is_list(Keys) ->
  66. design_doc_view(Req, Db, DName, ViewName, Keys);
  67. _ ->
  68. throw({bad_request, "`keys` member must be a array."})
  69. end;
  70. handle_view_req(Req, _Db, _DDoc) ->
  71. send_method_not_allowed(Req, "GET,POST,HEAD").
  72. handle_temp_view_req(#httpd{method='POST'}=Req, Db) ->
  73. couch_httpd:validate_ctype(Req, "application/json"),
  74. ok = couch_db:check_is_admin(Db),
  75. couch_stats_collector:increment({httpd, temporary_view_reads}),
  76. {Props} = couch_httpd:json_body_obj(Req),
  77. Language = couch_util:get_value(<<"language">>, Props, <<"javascript">>),
  78. {DesignOptions} = couch_util:get_value(<<"options">>, Props, {[]}),
  79. MapSrc = couch_util:get_value(<<"map">>, Props),
  80. Keys = couch_util:get_value(<<"keys">>, Props, nil),
  81. Reduce = get_reduce_type(Req),
  82. case couch_util:get_value(<<"reduce">>, Props, null) of
  83. null ->
  84. {ok, View, Group} = couch_view:get_temp_map_view(Db, Language,
  85. DesignOptions, MapSrc),
  86. QueryArgs = parse_view_params(Req, Keys, map, view_collator(View)),
  87. output_map_view(Req, View, Group, Db, QueryArgs, Keys);
  88. _ when Reduce =:= false ->
  89. {ok, View, Group} = couch_view:get_temp_map_view(Db, Language,
  90. DesignOptions, MapSrc),
  91. QueryArgs = parse_view_params(Req, Keys, red_map, view_collator(View)),
  92. output_map_view(Req, View, Group, Db, QueryArgs, Keys);
  93. RedSrc ->
  94. {ok, View, Group} = couch_view:get_temp_reduce_view(Db, Language,
  95. DesignOptions, MapSrc, RedSrc),
  96. QueryArgs = parse_view_params(Req, Keys, reduce, view_collator(View)),
  97. output_reduce_view(Req, Db, View, Group, QueryArgs, Keys)
  98. end;
  99. handle_temp_view_req(Req, _Db) ->
  100. send_method_not_allowed(Req, "POST").
  101. output_map_view(Req, View, Group, Db, QueryArgs, nil) ->
  102. #view_query_args{
  103. limit = Limit,
  104. skip = SkipCount
  105. } = QueryArgs,
  106. CurrentEtag = view_etag(Db, Group, View, QueryArgs),
  107. couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
  108. {ok, RowCount} = couch_view:get_row_count(View),
  109. 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}),
  110. FoldAccInit = {Limit, SkipCount, undefined, []},
  111. {ok, LastReduce, FoldResult} = couch_view:fold(View,
  112. FoldlFun, FoldAccInit, make_key_options(QueryArgs)),
  113. finish_view_fold(Req, RowCount,
  114. couch_view:reduce_to_count(LastReduce), FoldResult)
  115. end);
  116. output_map_view(Req, View, Group, Db, QueryArgs, Keys) ->
  117. #view_query_args{
  118. limit = Limit,
  119. skip = SkipCount
  120. } = QueryArgs,
  121. CurrentEtag = view_etag(Db, Group, View, QueryArgs, Keys),
  122. couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
  123. {ok, RowCount} = couch_view:get_row_count(View),
  124. FoldAccInit = {Limit, SkipCount, undefined, []},
  125. {LastReduce, FoldResult} = lists:foldl(fun(Key, {_, FoldAcc}) ->
  126. FoldlFun = make_view_fold_fun(Req, QueryArgs#view_query_args{},
  127. CurrentEtag, Db, Group#group.current_seq, RowCount,
  128. #view_fold_helper_funs{
  129. reduce_count = fun couch_view:reduce_to_count/1
  130. }),
  131. {ok, LastReduce, FoldResult} = couch_view:fold(View, FoldlFun,
  132. FoldAcc, make_key_options(
  133. QueryArgs#view_query_args{start_key=Key, end_key=Key})),
  134. {LastReduce, FoldResult}
  135. end, {{[],[]}, FoldAccInit}, Keys),
  136. finish_view_fold(Req, RowCount, couch_view:reduce_to_count(LastReduce),
  137. FoldResult, [{update_seq,Group#group.current_seq}])
  138. end).
  139. output_reduce_view(Req, Db, View, Group, QueryArgs, nil) ->
  140. #view_query_args{
  141. limit = Limit,
  142. skip = Skip,
  143. group_level = GroupLevel
  144. } = QueryArgs,
  145. CurrentEtag = view_etag(Db, Group, View, QueryArgs),
  146. couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
  147. {ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel,
  148. QueryArgs, CurrentEtag, Group#group.current_seq,
  149. #reduce_fold_helper_funs{}),
  150. FoldAccInit = {Limit, Skip, undefined, []},
  151. {ok, {_, _, Resp, _}} = couch_view:fold_reduce(View,
  152. RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} |
  153. make_key_options(QueryArgs)]),
  154. finish_reduce_fold(Req, Resp)
  155. end);
  156. output_reduce_view(Req, Db, View, Group, QueryArgs, Keys) ->
  157. #view_query_args{
  158. limit = Limit,
  159. skip = Skip,
  160. group_level = GroupLevel
  161. } = QueryArgs,
  162. CurrentEtag = view_etag(Db, Group, View, QueryArgs, Keys),
  163. couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
  164. {ok, GroupRowsFun, RespFun} = make_reduce_fold_funs(Req, GroupLevel,
  165. QueryArgs, CurrentEtag, Group#group.current_seq,
  166. #reduce_fold_helper_funs{}),
  167. {Resp, _RedAcc3} = lists:foldl(
  168. fun(Key, {Resp, RedAcc}) ->
  169. % run the reduce once for each key in keys, with limit etc
  170. % reapplied for each key
  171. FoldAccInit = {Limit, Skip, Resp, RedAcc},
  172. {_, {_, _, Resp2, RedAcc2}} = couch_view:fold_reduce(View,
  173. RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} |
  174. make_key_options(QueryArgs#view_query_args{
  175. start_key=Key, end_key=Key})]),
  176. % Switch to comma
  177. {Resp2, RedAcc2}
  178. end,
  179. {undefined, []}, Keys), % Start with no comma
  180. finish_reduce_fold(Req, Resp, [{update_seq,Group#group.current_seq}])
  181. end).
  182. reverse_key_default(?MIN_STR) -> ?MAX_STR;
  183. reverse_key_default(?MAX_STR) -> ?MIN_STR;
  184. reverse_key_default(Key) -> Key.
  185. get_stale_type(Req) ->
  186. list_to_existing_atom(couch_httpd:qs_value(Req, "stale", "nil")).
  187. get_reduce_type(Req) ->
  188. list_to_existing_atom(couch_httpd:qs_value(Req, "reduce", "true")).
  189. load_view(Req, Db, {ViewDesignId, ViewName}, Keys) ->
  190. Stale = get_stale_type(Req),
  191. Reduce = get_reduce_type(Req),
  192. case couch_view:get_map_view(Db, ViewDesignId, ViewName, Stale) of
  193. {ok, View, Group} ->
  194. QueryArgs = parse_view_params(Req, Keys, map, view_collator(View)),
  195. {map, View, Group, QueryArgs};
  196. {not_found, _Reason} ->
  197. case couch_view:get_reduce_view(Db, ViewDesignId, ViewName, Stale) of
  198. {ok, ReduceView, Group} ->
  199. Collator = view_collator(ReduceView),
  200. case Reduce of
  201. false ->
  202. QueryArgs = parse_view_params(Req, Keys, map_red, Collator),
  203. MapView = couch_view:extract_map_view(ReduceView),
  204. {map, MapView, Group, QueryArgs};
  205. _ ->
  206. QueryArgs = parse_view_params(Req, Keys, reduce, Collator),
  207. {reduce, ReduceView, Group, QueryArgs}
  208. end;
  209. {not_found, Reason} ->
  210. throw({not_found, Reason})
  211. end
  212. end.
  213. view_collator({reduce, _N, _Lang, View}) ->
  214. view_collator(View);
  215. view_collator({temp_reduce, View}) ->
  216. view_collator(View);
  217. view_collator(#view{btree=Btree}) ->
  218. % Return an "is-less-than" predicate by calling into the btree's
  219. % collator. For raw collation, couch_btree compares arbitrary
  220. % Erlang terms, but for normal (ICU) collation, it expects
  221. % {Json, Id} tuples.
  222. fun
  223. ({_JsonA, _IdA}=A, {_JsonB, _IdB}=B) ->
  224. couch_btree:less(Btree, A, B);
  225. (JsonA, JsonB) ->
  226. couch_btree:less(Btree, {JsonA, null}, {JsonB, null})
  227. end.
  228. % query_parse_error could be removed
  229. % we wouldn't need to pass the view type, it'd just parse params.
  230. % I'm not sure what to do about the error handling, but
  231. % it might simplify things to have a parse_view_params function
  232. % that doesn't throw().
  233. parse_view_params(Req, Keys, ViewType, LessThan) ->
  234. QueryList = couch_httpd:qs(Req),
  235. QueryParams =
  236. lists:foldl(fun({K, V}, Acc) ->
  237. parse_view_param(K, V) ++ Acc
  238. end, [], QueryList),
  239. IsMultiGet = (Keys =/= nil),
  240. Args = #view_query_args{
  241. view_type=ViewType,
  242. multi_get=IsMultiGet
  243. },
  244. QueryArgs = lists:foldl(fun({K, V}, Args2) ->
  245. validate_view_query(K, V, Args2)
  246. end, Args, lists:reverse(QueryParams)), % Reverse to match QS order.
  247. warn_on_empty_key_range(QueryArgs, LessThan),
  248. GroupLevel = QueryArgs#view_query_args.group_level,
  249. case {ViewType, GroupLevel, IsMultiGet} of
  250. {reduce, exact, true} ->
  251. QueryArgs;
  252. {reduce, _, false} ->
  253. QueryArgs;
  254. {reduce, _, _} ->
  255. % we can simplify code if we just drop this error message.
  256. Msg = <<"Multi-key fetchs for reduce "
  257. "view must include `group=true`">>,
  258. throw({query_parse_error, Msg});
  259. _ ->
  260. QueryArgs
  261. end,
  262. QueryArgs.
  263. parse_view_param("", _) ->
  264. [];
  265. parse_view_param("key", Value) ->
  266. JsonKey = ?JSON_DECODE(Value),
  267. [{start_key, JsonKey}, {end_key, JsonKey}];
  268. % TODO: maybe deprecate startkey_docid
  269. parse_view_param("startkey_docid", Value) ->
  270. [{start_docid, ?l2b(Value)}];
  271. parse_view_param("start_key_doc_id", Value) ->
  272. [{start_docid, ?l2b(Value)}];
  273. % TODO: maybe deprecate endkey_docid
  274. parse_view_param("endkey_docid", Value) ->
  275. [{end_docid, ?l2b(Value)}];
  276. parse_view_param("end_key_doc_id", Value) ->
  277. [{end_docid, ?l2b(Value)}];
  278. % TODO: maybe deprecate startkey
  279. parse_view_param("startkey", Value) ->
  280. [{start_key, ?JSON_DECODE(Value)}];
  281. parse_view_param("start_key", Value) ->
  282. [{start_key, ?JSON_DECODE(Value)}];
  283. % TODO: maybe deprecate endkey
  284. parse_view_param("endkey", Value) ->
  285. [{end_key, ?JSON_DECODE(Value)}];
  286. parse_view_param("end_key", Value) ->
  287. [{end_key, ?JSON_DECODE(Value)}];
  288. parse_view_param("limit", Value) ->
  289. [{limit, parse_positive_int_param(Value)}];
  290. parse_view_param("count", _Value) ->
  291. throw({query_parse_error, <<"Query parameter 'count' is now 'limit'.">>});
  292. parse_view_param("stale", "ok") ->
  293. [{stale, ok}];
  294. parse_view_param("stale", "update_after") ->
  295. [{stale, update_after}];
  296. parse_view_param("stale", _Value) ->
  297. throw({query_parse_error,
  298. <<"stale only available as stale=ok or as stale=update_after">>});
  299. parse_view_param("update", _Value) ->
  300. throw({query_parse_error, <<"update=false is now stale=ok">>});
  301. parse_view_param("descending", Value) ->
  302. [{descending, parse_bool_param(Value)}];
  303. parse_view_param("skip", Value) ->
  304. [{skip, parse_int_param(Value)}];
  305. parse_view_param("group", Value) ->
  306. case parse_bool_param(Value) of
  307. true -> [{group_level, exact}];
  308. false -> [{group_level, 0}]
  309. end;
  310. parse_view_param("group_level", Value) ->
  311. [{group_level, parse_positive_int_param(Value)}];
  312. parse_view_param("inclusive_end", Value) ->
  313. [{inclusive_end, parse_bool_param(Value)}];
  314. parse_view_param("reduce", Value) ->
  315. [{reduce, parse_bool_param(Value)}];
  316. parse_view_param("include_docs", Value) ->
  317. [{include_docs, parse_bool_param(Value)}];
  318. parse_view_param("conflicts", Value) ->
  319. [{conflicts, parse_bool_param(Value)}];
  320. parse_view_param("list", Value) ->
  321. [{list, ?l2b(Value)}];
  322. parse_view_param("callback", _) ->
  323. []; % Verified in the JSON response functions
  324. parse_view_param(Key, Value) ->
  325. [{extra, {Key, Value}}].
  326. warn_on_empty_key_range(#view_query_args{start_key=undefined}, _Lt) ->
  327. ok;
  328. warn_on_empty_key_range(#view_query_args{end_key=undefined}, _Lt) ->
  329. ok;
  330. warn_on_empty_key_range(#view_query_args{start_key=A, end_key=A}, _Lt) ->
  331. ok;
  332. warn_on_empty_key_range(#view_query_args{
  333. start_key=StartKey, end_key=EndKey, direction=Dir}, LessThan) ->
  334. case {Dir, LessThan(StartKey, EndKey)} of
  335. {fwd, false} ->
  336. throw({query_parse_error,
  337. <<"No rows can match your key range, reverse your ",
  338. "start_key and end_key or set descending=true">>});
  339. {rev, true} ->
  340. throw({query_parse_error,
  341. <<"No rows can match your key range, reverse your ",
  342. "start_key and end_key or set descending=false">>});
  343. _ -> ok
  344. end.
  345. validate_view_query(start_key, Value, Args) ->
  346. case Args#view_query_args.multi_get of
  347. true ->
  348. Msg = <<"Query parameter `start_key` is "
  349. "not compatible with multi-get">>,
  350. throw({query_parse_error, Msg});
  351. _ ->
  352. Args#view_query_args{start_key=Value}
  353. end;
  354. validate_view_query(start_docid, Value, Args) ->
  355. Args#view_query_args{start_docid=Value};
  356. validate_view_query(end_key, Value, Args) ->
  357. case Args#view_query_args.multi_get of
  358. true->
  359. Msg = <<"Query parameter `end_key` is "
  360. "not compatible with multi-get">>,
  361. throw({query_parse_error, Msg});
  362. _ ->
  363. Args#view_query_args{end_key=Value}
  364. end;
  365. validate_view_query(end_docid, Value, Args) ->
  366. Args#view_query_args{end_docid=Value};
  367. validate_view_query(limit, Value, Args) ->
  368. Args#view_query_args{limit=Value};
  369. validate_view_query(list, Value, Args) ->
  370. Args#view_query_args{list=Value};
  371. validate_view_query(stale, ok, Args) ->
  372. Args#view_query_args{stale=ok};
  373. validate_view_query(stale, update_after, Args) ->
  374. Args#view_query_args{stale=update_after};
  375. validate_view_query(stale, _, Args) ->
  376. Args;
  377. validate_view_query(descending, true, Args) ->
  378. case Args#view_query_args.direction of
  379. rev -> Args; % Already reversed
  380. fwd ->
  381. Args#view_query_args{
  382. direction = rev,
  383. start_docid =
  384. reverse_key_default(Args#view_query_args.start_docid),
  385. end_docid =
  386. reverse_key_default(Args#view_query_args.end_docid)
  387. }
  388. end;
  389. validate_view_query(descending, false, Args) ->
  390. Args; % Ignore default condition
  391. validate_view_query(skip, Value, Args) ->
  392. Args#view_query_args{skip=Value};
  393. validate_view_query(group_level, Value, Args) ->
  394. case Args#view_query_args.view_type of
  395. reduce ->
  396. Args#view_query_args{group_level=Value};
  397. _ ->
  398. Msg = <<"Invalid URL parameter 'group' or "
  399. " 'group_level' for non-reduce view.">>,
  400. throw({query_parse_error, Msg})
  401. end;
  402. validate_view_query(inclusive_end, Value, Args) ->
  403. Args#view_query_args{inclusive_end=Value};
  404. validate_view_query(reduce, false, Args) ->
  405. Args;
  406. validate_view_query(reduce, _, Args) ->
  407. case Args#view_query_args.view_type of
  408. map ->
  409. Msg = <<"Invalid URL parameter `reduce` for map view.">>,
  410. throw({query_parse_error, Msg});
  411. _ ->
  412. Args
  413. end;
  414. validate_view_query(include_docs, true, Args) ->
  415. case Args#view_query_args.view_type of
  416. reduce ->
  417. Msg = <<"Query parameter `include_docs` "
  418. "is invalid for reduce views.">>,
  419. throw({query_parse_error, Msg});
  420. _ ->
  421. Args#view_query_args{include_docs=true}
  422. end;
  423. % Use the view_query_args record's default value
  424. validate_view_query(include_docs, _Value, Args) ->
  425. Args;
  426. validate_view_query(conflicts, true, Args) ->
  427. case Args#view_query_args.view_type of
  428. reduce ->
  429. Msg = <<"Query parameter `conflicts` "
  430. "is invalid for reduce views.">>,
  431. throw({query_parse_error, Msg});
  432. _ ->
  433. Args#view_query_args{conflicts = true}
  434. end;
  435. validate_view_query(extra, _Value, Args) ->
  436. Args.
  437. make_view_fold_fun(Req, QueryArgs, Etag, Db, UpdateSeq, TotalViewCount, HelperFuns) ->
  438. #view_fold_helper_funs{
  439. start_response = StartRespFun,
  440. send_row = SendRowFun,
  441. reduce_count = ReduceCountFun
  442. } = apply_default_helper_funs(HelperFuns),
  443. #view_query_args{
  444. include_docs = IncludeDocs,
  445. conflicts = Conflicts
  446. } = QueryArgs,
  447. fun({{Key, DocId}, Value}, OffsetReds,
  448. {AccLimit, AccSkip, Resp, RowFunAcc}) ->
  449. case {AccLimit, AccSkip, Resp} of
  450. {0, _, _} ->
  451. % we've done "limit" rows, stop foldling
  452. {stop, {0, 0, Resp, RowFunAcc}};
  453. {_, AccSkip, _} when AccSkip > 0 ->
  454. % just keep skipping
  455. {ok, {AccLimit, AccSkip - 1, Resp, RowFunAcc}};
  456. {_, _, undefined} ->
  457. % rendering the first row, first we start the response
  458. Offset = ReduceCountFun(OffsetReds),
  459. {ok, Resp2, RowFunAcc0} = StartRespFun(Req, Etag,
  460. TotalViewCount, Offset, RowFunAcc, UpdateSeq),
  461. {Go, RowFunAcc2} = SendRowFun(Resp2, Db, {{Key, DocId}, Value},
  462. IncludeDocs, Conflicts, RowFunAcc0),
  463. {Go, {AccLimit - 1, 0, Resp2, RowFunAcc2}};
  464. {AccLimit, _, Resp} when (AccLimit > 0) ->
  465. % rendering all other rows
  466. {Go, RowFunAcc2} = SendRowFun(Resp, Db, {{Key, DocId}, Value},
  467. IncludeDocs, Conflicts, RowFunAcc),
  468. {Go, {AccLimit - 1, 0, Resp, RowFunAcc2}}
  469. end
  470. end.
  471. make_reduce_fold_funs(Req, GroupLevel, _QueryArgs, Etag, UpdateSeq, HelperFuns) ->
  472. #reduce_fold_helper_funs{
  473. start_response = StartRespFun,
  474. send_row = SendRowFun
  475. } = apply_default_helper_funs(HelperFuns),
  476. GroupRowsFun =
  477. fun({_Key1,_}, {_Key2,_}) when GroupLevel == 0 ->
  478. true;
  479. ({Key1,_}, {Key2,_})
  480. when is_integer(GroupLevel) and is_list(Key1) and is_list(Key2) ->
  481. lists:sublist(Key1, GroupLevel) == lists:sublist(Key2, GroupLevel);
  482. ({Key1,_}, {Key2,_}) ->
  483. Key1 == Key2
  484. end,
  485. RespFun = fun
  486. (_Key, _Red, {AccLimit, AccSkip, Resp, RowAcc}) when AccSkip > 0 ->
  487. % keep skipping
  488. {ok, {AccLimit, AccSkip - 1, Resp, RowAcc}};
  489. (_Key, _Red, {0, _AccSkip, Resp, RowAcc}) ->
  490. % we've exhausted limit rows, stop
  491. {stop, {0, _AccSkip, Resp, RowAcc}};
  492. (_Key, Red, {AccLimit, 0, undefined, RowAcc0}) when GroupLevel == 0 ->
  493. % we haven't started responding yet and group=false
  494. {ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0, UpdateSeq),
  495. {Go, RowAcc2} = SendRowFun(Resp2, {null, Red}, RowAcc),
  496. {Go, {AccLimit - 1, 0, Resp2, RowAcc2}};
  497. (_Key, Red, {AccLimit, 0, Resp, RowAcc}) when GroupLevel == 0 ->
  498. % group=false but we've already started the response
  499. {Go, RowAcc2} = SendRowFun(Resp, {null, Red}, RowAcc),
  500. {Go, {AccLimit - 1, 0, Resp, RowAcc2}};
  501. (Key, Red, {AccLimit, 0, undefined, RowAcc0})
  502. when is_integer(GroupLevel), is_list(Key) ->
  503. % group_level and we haven't responded yet
  504. {ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0, UpdateSeq),
  505. {Go, RowAcc2} = SendRowFun(Resp2,
  506. {lists:sublist(Key, GroupLevel), Red}, RowAcc),
  507. {Go, {AccLimit - 1, 0, Resp2, RowAcc2}};
  508. (Key, Red, {AccLimit, 0, Resp, RowAcc})
  509. when is_integer(GroupLevel), is_list(Key) ->
  510. % group_level and we've already started the response
  511. {Go, RowAcc2} = SendRowFun(Resp,
  512. {lists:sublist(Key, GroupLevel), Red}, RowAcc),
  513. {Go, {AccLimit - 1, 0, Resp, RowAcc2}};
  514. (Key, Red, {AccLimit, 0, undefined, RowAcc0}) ->
  515. % group=true and we haven't responded yet
  516. {ok, Resp2, RowAcc} = StartRespFun(Req, Etag, RowAcc0, UpdateSeq),
  517. {Go, RowAcc2} = SendRowFun(Resp2, {Key, Red}, RowAcc),
  518. {Go, {AccLimit - 1, 0, Resp2, RowAcc2}};
  519. (Key, Red, {AccLimit, 0, Resp, RowAcc}) ->
  520. % group=true and we've already started the response
  521. {Go, RowAcc2} = SendRowFun(Resp, {Key, Red}, RowAcc),
  522. {Go, {AccLimit - 1, 0, Resp, RowAcc2}}
  523. end,
  524. {ok, GroupRowsFun, RespFun}.
  525. apply_default_helper_funs(
  526. #view_fold_helper_funs{
  527. start_response = StartResp,
  528. send_row = SendRow
  529. }=Helpers) ->
  530. StartResp2 = case StartResp of
  531. undefined -> fun json_view_start_resp/6;
  532. _ -> StartResp
  533. end,
  534. SendRow2 = case SendRow of
  535. undefined -> fun send_json_view_row/6;
  536. _ -> SendRow
  537. end,
  538. Helpers#view_fold_helper_funs{
  539. start_response = StartResp2,
  540. send_row = SendRow2
  541. };
  542. apply_default_helper_funs(
  543. #reduce_fold_helper_funs{
  544. start_response = StartResp,
  545. send_row = SendRow
  546. }=Helpers) ->
  547. StartResp2 = case StartResp of
  548. undefined -> fun json_reduce_start_resp/4;
  549. _ -> StartResp
  550. end,
  551. SendRow2 = case SendRow of
  552. undefined -> fun send_json_reduce_row/3;
  553. _ -> SendRow
  554. end,
  555. Helpers#reduce_fold_helper_funs{
  556. start_response = StartResp2,
  557. send_row = SendRow2
  558. }.
  559. make_key_options(#view_query_args{direction = Dir}=QueryArgs) ->
  560. [{dir,Dir} | make_start_key_option(QueryArgs) ++
  561. make_end_key_option(QueryArgs)].
  562. make_start_key_option(
  563. #view_query_args{
  564. start_key = StartKey,
  565. start_docid = StartDocId}) ->
  566. if StartKey == undefined ->
  567. [];
  568. true ->
  569. [{start_key, {StartKey, StartDocId}}]
  570. end.
  571. make_end_key_option(#view_query_args{end_key = undefined}) ->
  572. [];
  573. make_end_key_option(
  574. #view_query_args{end_key = EndKey,
  575. end_docid = EndDocId,
  576. inclusive_end = true}) ->
  577. [{end_key, {EndKey, EndDocId}}];
  578. make_end_key_option(
  579. #view_query_args{
  580. end_key = EndKey,
  581. end_docid = EndDocId,
  582. inclusive_end = false}) ->
  583. [{end_key_gt, {EndKey,reverse_key_default(EndDocId)}}].
  584. json_view_start_resp(Req, Etag, TotalViewCount, Offset, _Acc, UpdateSeq) ->
  585. {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]),
  586. BeginBody = case couch_httpd:qs_value(Req, "update_seq") of
  587. "true" ->
  588. io_lib:format(
  589. "{\"total_rows\":~w,\"update_seq\":~w,"
  590. "\"offset\":~w,\"rows\":[\r\n",
  591. [TotalViewCount, UpdateSeq, Offset]);
  592. _Else ->
  593. io_lib:format(
  594. "{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n",
  595. [TotalViewCount, Offset])
  596. end,
  597. {ok, Resp, BeginBody}.
  598. send_json_view_row(Resp, Db, Kv, IncludeDocs, Conflicts, RowFront) ->
  599. JsonObj = view_row_obj(Db, Kv, IncludeDocs, Conflicts),
  600. send_chunk(Resp, RowFront ++ ?JSON_ENCODE(JsonObj)),
  601. {ok, ",\r\n"}.
  602. json_reduce_start_resp(Req, Etag, _Acc0, UpdateSeq) ->
  603. {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]),
  604. case couch_httpd:qs_value(Req, "update_seq") of
  605. "true" ->
  606. {ok, Resp, io_lib:format("{\"update_seq\":~w,\"rows\":[\r\n",[UpdateSeq])};
  607. _Else ->
  608. {ok, Resp, "{\"rows\":[\r\n"}
  609. end.
  610. send_json_reduce_row(Resp, {Key, Value}, RowFront) ->
  611. send_chunk(Resp, RowFront ++ ?JSON_ENCODE({[{key, Key}, {value, Value}]})),
  612. {ok, ",\r\n"}.
  613. view_etag(Db, Group, View, QueryArgs) ->
  614. view_etag(Db, Group, View, QueryArgs, nil).
  615. view_etag(Db, Group, {reduce, _, _, View}, QueryArgs, Extra) ->
  616. view_etag(Db, Group, View, QueryArgs, Extra);
  617. view_etag(Db, Group, {temp_reduce, View}, QueryArgs, Extra) ->
  618. view_etag(Db, Group, View, QueryArgs, Extra);
  619. view_etag(_Db, #group{sig=Sig, current_seq=CurrentSeq}, _View, #view_query_args{include_docs=true}, Extra) ->
  620. couch_httpd:make_etag({Sig, CurrentSeq, Extra});
  621. view_etag(_Db, #group{sig=Sig}, #view{update_seq=UpdateSeq, purge_seq=PurgeSeq}, _QueryArgs, Extra) ->
  622. couch_httpd:make_etag({Sig, UpdateSeq, PurgeSeq, Extra}).
  623. % the view row has an error
  624. view_row_obj(_Db, {{Key, error}, Value}, _IncludeDocs, _Conflicts) ->
  625. {[{key, Key}, {error, Value}]};
  626. % include docs in the view output
  627. view_row_obj(Db, {{Key, DocId}, {Props}}, true, Conflicts) ->
  628. Rev = case couch_util:get_value(<<"_rev">>, Props) of
  629. undefined ->
  630. nil;
  631. Rev0 ->
  632. couch_doc:parse_rev(Rev0)
  633. end,
  634. IncludeId = couch_util:get_value(<<"_id">>, Props, DocId),
  635. view_row_with_doc(Db, {{Key, DocId}, {Props}}, {IncludeId, Rev}, Conflicts);
  636. view_row_obj(Db, {{Key, DocId}, Value}, true, Conflicts) ->
  637. view_row_with_doc(Db, {{Key, DocId}, Value}, {DocId, nil}, Conflicts);
  638. % the normal case for rendering a view row
  639. view_row_obj(_Db, {{Key, DocId}, Value}, _IncludeDocs, _Conflicts) ->
  640. {[{id, DocId}, {key, Key}, {value, Value}]}.
  641. view_row_with_doc(Db, {{Key, DocId}, Value}, IdRev, Conflicts) ->
  642. {[{id, DocId}, {key, Key}, {value, Value}] ++
  643. doc_member(Db, IdRev, if Conflicts -> [conflicts]; true -> [] end)}.
  644. doc_member(Db, #doc_info{id = Id, revs = [#rev_info{rev = Rev} | _]} = Info,
  645. Options) ->
  646. ?LOG_DEBUG("Include Doc: ~p ~p", [Id, Rev]),
  647. case couch_db:open_doc(Db, Info, [deleted | Options]) of
  648. {ok, Doc} ->
  649. [{doc, couch_doc:to_json_obj(Doc, [])}];
  650. _ ->
  651. [{doc, null}]
  652. end;
  653. doc_member(Db, {DocId, Rev}, Options) ->
  654. ?LOG_DEBUG("Include Doc: ~p ~p", [DocId, Rev]),
  655. case (catch couch_httpd_db:couch_doc_open(Db, DocId, Rev, Options)) of
  656. #doc{} = Doc ->
  657. JsonDoc = couch_doc:to_json_obj(Doc, []),
  658. [{doc, JsonDoc}];
  659. _Else ->
  660. [{doc, null}]
  661. end.
  662. finish_view_fold(Req, TotalRows, Offset, FoldResult) ->
  663. finish_view_fold(Req, TotalRows, Offset, FoldResult, []).
  664. finish_view_fold(Req, TotalRows, Offset, FoldResult, Fields) ->
  665. case FoldResult of
  666. {_, _, undefined, _} ->
  667. % nothing found in the view or keys, nothing has been returned
  668. % send empty view
  669. send_json(Req, 200, {[
  670. {total_rows, TotalRows},
  671. {offset, Offset},
  672. {rows, []}
  673. ] ++ Fields});
  674. {_, _, Resp, _} ->
  675. % end the view
  676. send_chunk(Resp, "\r\n]}"),
  677. end_json_response(Resp)
  678. end.
  679. finish_reduce_fold(Req, Resp) ->
  680. finish_reduce_fold(Req, Resp, []).
  681. finish_reduce_fold(Req, Resp, Fields) ->
  682. case Resp of
  683. undefined ->
  684. send_json(Req, 200, {[
  685. {rows, []}
  686. ] ++ Fields});
  687. Resp ->
  688. send_chunk(Resp, "\r\n]}"),
  689. end_json_response(Resp)
  690. end.
  691. parse_bool_param(Val) ->
  692. case string:to_lower(Val) of
  693. "true" -> true;
  694. "false" -> false;
  695. _ ->
  696. Msg = io_lib:format("Invalid boolean parameter: ~p", [Val]),
  697. throw({query_parse_error, ?l2b(Msg)})
  698. end.
  699. parse_int_param(Val) ->
  700. case (catch list_to_integer(Val)) of
  701. IntVal when is_integer(IntVal) ->
  702. IntVal;
  703. _ ->
  704. Msg = io_lib:format("Invalid value for integer parameter: ~p", [Val]),
  705. throw({query_parse_error, ?l2b(Msg)})
  706. end.
  707. parse_positive_int_param(Val) ->
  708. case parse_int_param(Val) of
  709. IntVal when IntVal >= 0 ->
  710. IntVal;
  711. _ ->
  712. Fmt = "Invalid value for positive integer parameter: ~p",
  713. Msg = io_lib:format(Fmt, [Val]),
  714. throw({query_parse_error, ?l2b(Msg)})
  715. end.