%%% -*- erlang -*-
%%%
%%% This file is part of couchbeam released under the MIT license.
%%% See the NOTICE for more information.
-module(couchbeam_view).
-author('BenoƮt Chesneau Db: a db record ViewName: See {@link couchbeam_view:stream/4} for more information about
%% options. Return: {ok, Rows} or {error, Error} Db: a db record ViewName: 'all_docs' to get all docs or {DesignName,
%% ViewName} Client: pid where to send view events where events are:
%% 'all_docs' to get all docs or {DesignName,
%% ViewName}Options :: view_options() [{key, binary()} | {start_docid, binary()}
%% | {end_docid, binary()} | {start_key, binary()}
%% | {end_key, binary()} | {limit, integer()}
%% | {stale, stale()}
%% | descending
%% | {skip, integer()}
%% | group | {group_level, integer()}
%% | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
%% | {keys, list(binary())}
%%
%%
Options :: view_options() [{key, binary()} | {start_docid, binary()}
%% | {end_docid, binary()} | {start_key, binary()}
%% | {end_key, binary()} | {limit, integer()}
%% | {stale, stale()}
%% | descending
%% | {skip, integer()}
%% | group | {group_level, integer()}
%% | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
%% | {keys, list(binary())}
%% | `{stream_to, Pid}': the pid where the changes will be sent,
%% by default the current pid. Used for continuous and longpoll
%% connections
%%
%% {key, Key}: key value{start_docid, DocId}: document id to start with (to allow pagination
%% for duplicate start keys{end_docid, DocId}: last document id to include in the result (to
%% allow pagination for duplicate endkeys){start_key, Key}: start result from key value{end_key, Key}: end result from key value{limit, Limit}: Limit the number of documents in the result{stale, Stale}: If stale=ok is set, CouchDB will not refresh the view
%% even if it is stale, the benefit is a an improved query latency. If
%% stale=update_after is set, CouchDB will update the view after the stale
%% result is returned.descending: reverse the result{skip, N}: skip n number of documentsgroup: the reduce function reduces to a single result
%% row.{group_level, Level}: the reduce function reduces to a set
%% of distinct keys.{reduce, boolean()}: whether to use the reduce function of the view. It defaults to
%% true, if a reduce function is defined and to false otherwise.include_docs: automatically fetch and include the document
%% which emitted each view entry{inclusive_end, boolean()}: Controls whether the endkey is included in
%% the result. It defaults to true.conflicts: include conflicts{keys, [Keys]}: to pass multiple keys to the view query Return {ok, StartRef, ViewPid} or {error,
%Error}. Ref can be
%% used to disctint all changes from this pid. ViewPid is the pid of
%% the view loop process. Can be used to monitor it or kill it
%% when needed.
Db: a db record
%%ViewName: 'all_docs' to get all docs or {DesignName, %% ViewName}
%%Options :: view_options() [{key, binary()} | {start_docid, binary()}
%% | {end_docid, binary()} | {start_key, binary()}
%% | {end_key, binary()} | {limit, integer()}
%% | {stale, stale()}
%% | descending
%% | {skip, integer()}
%% | group | {group_level, integer()}
%% | {inclusive_end, boolean()} | {reduce, boolean()} | reduce | include_docs | conflicts
%% | {keys, list(binary())}
%% See {@link couchbeam_view:stream/4} for more information about %% options.
%%Return: {ok, Row} or {error, Error}
first(Db, ViewName, Options) -> %% we only want 1 result so force the limit to 1. no need to fetch %% all the results Options1 = couchbeam_util:force_param(limit, 1, Options), %% make the request make_view(Db, ViewName, Options1, fun(Args, Url) -> case view_request(Db, Url, Args) of {ok, _, _, Ref} -> {Props} = couchbeam_httpc:json_body(Ref), case couchbeam_util:get_value(<<"rows">>, Props) of [] -> {ok, nil}; [Row] -> {ok, Row} end; Error -> Error end end). -spec fold(Function::function(), Acc::any(), Db::db(), ViewName::'all_docs' | {DesignName::design_name(), ViewName::view_name()}) -> list(term()) | {error, term()}. %% @equiv fold(Function, Acc, Db, ViewName, []) fold(Function, Acc, Db, ViewName) -> fold(Function, Acc, Db, ViewName, []). -spec fold(Function::function(), Acc::any(), Db::db(), ViewName::'all_docs' | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> list(term()) | {error, term()}. %% @doc call Function(Row, AccIn) on succesive row, starting with %% AccIn == Acc. Function/2 must return a new list accumultator or the %% atom done to stop fetching results. Acc0 is returned if the %% list is empty. For example: %% ``` %% couchbeam_view:fold(fun(Row, Acc) -> [Row|Acc] end, [], Db, 'all_docs'). %% ''' fold(Function, Acc, Db, ViewName, Options) -> %% make sure we stream item by item so we can stop at any time. Options1 = couchbeam_util:force_param(async, once, Options), %% start iterrating the view results case stream(Db, ViewName, Options1) of {ok, Ref} -> fold_view_results(Ref, Function, Acc); Error -> Error end. -spec foreach(Function::function(), Db::db(), ViewName::'all_docs' | {DesignName::design_name(), ViewName::view_name()}) -> list(term()) | {error, term()}. %% @equiv foreach(Function, Db, ViewName, []) foreach(Function, Db, ViewName) -> foreach(Function, Db, ViewName, []). -spec foreach(Function::function(), Db::db(), ViewName::'all_docs' | {DesignName::design_name(), ViewName::view_name()}, Options::view_options()) -> list(term()) | {error, term()}. %% @doc call Function(Row) on succesive row. Example: %% ``` %% couchbeam_view:foreach(fun(Row) -> io:format("got row ~p~n", [Row]) end, Db, 'all_docs'). %% ''' foreach(Function, Db, ViewName, Options) -> FunWrapper = fun(Row, _Acc) -> Function(Row), ok end, fold(FunWrapper, ok, Db, ViewName, Options). %% ---------------------------------- %% utilities functions %% ---------------------------------- -spec parse_view_options(Options::list()) -> view_query_args(). %% @doc parse view options parse_view_options(Options) -> parse_view_options(Options, #view_query_args{}). parse_view_options([], Args) -> Args; parse_view_options([{key, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{key, couchbeam_ejson:encode(Value)}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{start_docid, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{start_docid, Value}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{end_docid, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{end_docid, Value}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{start_key, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{start_key, couchbeam_ejson:encode(Value)}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{end_key, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{end_key, couchbeam_ejson:encode(Value)}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{startkey, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{startkey, couchbeam_ejson:encode(Value)}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{endkey, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{endkey, couchbeam_ejson:encode(Value)}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{limit, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{limit, Value}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{stale, ok}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{stale, "ok"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{stale, update_after}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{stale, "update_after"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{stale, _}|_Rest], _Args) -> {error, "invalid stale value"}; parse_view_options([descending|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{descending, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([group|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{group, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{group_level, Level}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{group_level, Level}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([inclusive_end|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{inclusive_end, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{inclusive_end, true}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{inclusive_end, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{inclusive_end, false}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{inclusive_end, "false"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([reduce|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{reduce, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{reduce, true}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{reduce, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{reduce, false}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{reduce, "false"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([include_docs|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{include_docs, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([conflicts|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{conflicts, "true"}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{skip, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{skip, Value}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{list, Value}|Rest], #view_query_args{options=Opts}=Args) -> Opts1 = [{list, Value}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([{keys, Value}|Rest], Args) -> parse_view_options(Rest, Args#view_query_args{method=post, keys=Value}); parse_view_options([{Key, Value}|Rest], #view_query_args{options=Opts}=Args) when is_list(Key) -> Opts1 = [{Key, Value}|Opts], parse_view_options(Rest, Args#view_query_args{options=Opts1}); parse_view_options([_|Rest], Args) -> parse_view_options(Rest, Args). %% @private make_view(#db{server=Server}=Db, ViewName, Options, Fun) -> Args = parse_view_options(Options), case ViewName of 'all_docs' -> Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), [couchbeam_httpc:db_url(Db), <<"_all_docs">>], Args#view_query_args.options), Fun(Args, Url); {DName, VName} -> Url = hackney_url:make_url(couchbeam_httpc:server_url(Server), [couchbeam_httpc:db_url(Db), <<"_design">>, DName, <<"_view">>, VName], Args#view_query_args.options), Fun(Args, Url); _ -> {error, invalid_view_name} end. fold_view_results(Ref, Fun, Acc) -> receive {Ref, done} -> Acc; {Ref, {row, Row}} -> case Fun(Row, Acc) of stop -> cancel_stream(Ref), Acc; Acc1 -> stream_next(Ref), fold_view_results(Ref, Fun, Acc1) end; {Ref, Error} -> {error, Acc, Error} end. collect_view_results(Ref, Acc) -> receive {Ref, done} -> Rows = lists:reverse(Acc), {ok, Rows}; {Ref, {row, Row}} -> collect_view_results(Ref, [Row|Acc]); {Ref, {error, Error}} when Acc =:= []-> {error, Error}; {Ref, {error, Error}} -> %% in case we got some results Rows = lists:reverse(Acc), {error, Error, Rows} after 10000 -> {error, timeout} end. view_request(#db{options=Opts}, Url, Args) -> case Args#view_query_args.method of get -> couchbeam_httpc:db_request(get, Url, [], <<>>, Opts, [200]); post -> Body = couchbeam_ejson:encode( {[{<<"keys">>, Args#view_query_args.keys}]} ), Hdrs = [{<<"Content-Type">>, <<"application/json">>}], couchbeam_httpc:db_request(post, Url, Hdrs, Body, Opts, [200]) end. with_view_stream(Ref, Fun) -> case ets:lookup(couchbeam_view_streams, Ref) of [] -> {error, stream_undefined}; [{Ref, Pid}] -> Fun(Pid) end. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -include_lib("kernel/include/file.hrl"). clean_dbs() -> Server = couchbeam:server_connection(), catch couchbeam:delete_db(Server, "couchbeam_testdb"), ok. start_couchbeam_tests() -> {ok, _} = application:ensure_all_started(couchbeam), clean_dbs(). basic_test() -> start_couchbeam_tests(), Server = couchbeam:server_connection(), {ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"), DesignDoc = {[ {<<"_id">>, <<"_design/couchbeam">>}, {<<"language">>,<<"javascript">>}, {<<"views">>, {[{<<"test">>, {[{<<"map">>, <<"function (doc) {\n if (doc.type == \"test\") {\n emit(doc._id, doc);\n}\n}">> }]} },{<<"test2">>, {[{<<"map">>, <<"function (doc) {\n if (doc.type == \"test2\") {\n emit(doc._id, null);\n}\n}">> }]} }]} } ]}, Doc = {[ {<<"type">>, <<"test">>} ]}, couchbeam:save_docs(Db, [DesignDoc, Doc, Doc]), couchbeam:ensure_full_commit(Db), {ok, AllDocs} = couchbeam_view:fetch(Db), ?assertEqual(3, length(AllDocs)), {ok, Rst2} = couchbeam_view:fetch(Db, {"couchbeam", "test"}), ?assertEqual(2, length(Rst2)), Count = couchbeam_view:count(Db, {"couchbeam", "test"}), ?assertEqual(2, Count), {ok, {FirstRow}} = couchbeam_view:first(Db, {"couchbeam", "test"}, [include_docs]), {Doc1} = proplists:get_value(<<"doc">>, FirstRow), ?assertEqual(<<"test">>, proplists:get_value(<<"type">>, Doc1)), Docs = [ {[{<<"_id">>, <<"test1">>}, {<<"type">>, <<"test">>}, {<<"value">>, 1}]}, {[{<<"_id">>, <<"test2">>}, {<<"type">>, <<"test">>}, {<<"value">>, 2}]}, {[{<<"_id">>, <<"test3">>}, {<<"type">>, <<"test">>}, {<<"value">>, 3}]}, {[{<<"_id">>, <<"test4">>}, {<<"type">>, <<"test">>}, {<<"value">>, 4}]} ], couchbeam:save_docs(Db, Docs), couchbeam:ensure_full_commit(Db), {ok, Rst3} = couchbeam_view:fetch(Db, {"couchbeam", "test"}, [{start_key, <<"test">>}]), ?assertEqual(4, length(Rst3)), {ok, Rst4} = couchbeam_view:fetch(Db, {"couchbeam", "test"}, [{start_key, <<"test">>}, {end_key, <<"test3">>}]), ?assertEqual(3, length(Rst4)), AccFun = fun(Row, Acc) -> [Row | Acc] end, Rst5 = couchbeam_view:fold(AccFun, [], Db, {"couchbeam", "test"}, [{start_key, <<"test">>}, {end_key,<<"test3">>}]), ?assertEqual(3, length(Rst5)). view_notfound_test() -> start_couchbeam_tests(), Server = couchbeam:server_connection(), {ok, Db} = couchbeam:create_db(Server, "couchbeam_testdb"), {error, not_found} = couchbeam_view:fetch(Db, {"couchbeam", "test"}, []), ok. -endif.