diff --git a/src/chttpd/test/eunit/chttpd_changes_test.erl b/src/chttpd/test/eunit/chttpd_changes_test.erl index b08eb65fd0c..99ae0495fab 100644 --- a/src/chttpd/test/eunit/chttpd_changes_test.erl +++ b/src/chttpd/test/eunit/chttpd_changes_test.erl @@ -153,6 +153,18 @@ changes_include_docs_test_() -> ] }. +changes_open_doc_times_test_() -> + { + foreach, + fun setup_basic/0, + fun teardown_basic/1, + [ + ?TDEF_FE(t_selector_open_doc_times), + ?TDEF_FE(t_js_filter_open_doc_times), + ?TDEF_FE(t_view_open_doc_times) + ] + }. + t_basic({_, DbUrl}) -> Res = {Seq, Pending, Rows} = changes(DbUrl), ?assertEqual(8, Seq), @@ -853,8 +865,39 @@ t_view_all_docs_conflicts_include_docs({_, DbUrl}) -> ?assertEqual(Res1, Res2), delete_ddocs(DDocUrl, Rev). -% Utility functions +t_selector_open_doc_times({_, DbUrl}) -> + Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, + F = "?filter=_selector", + I = "&include_docs=true", + S = "&style=all_docs", + Filter = called_times(fun() -> changes_post(DbUrl, Body, F) end, DbUrl), + FilterDocs = called_times(fun() -> changes_post(DbUrl, Body, F ++ I) end, DbUrl), + FilterAllDocs = called_times(fun() -> changes_post(DbUrl, Body, F ++ I ++ S) end, DbUrl), + ?assertEqual({3, 3, 5}, {Filter, FilterDocs, FilterAllDocs}). + +t_js_filter_open_doc_times({_, DbUrl}) -> + {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, custom), + F = "?filter=filters/f", + I = "&include_docs=true", + S = "&style=all_docs", + Filter = called_times(fun() -> changes(DbUrl, F) end, DbUrl), + FilterDocs = called_times(fun() -> changes(DbUrl, F ++ I) end, DbUrl), + FilterAllDocs = called_times(fun() -> changes(DbUrl, F ++ I ++ S) end, DbUrl), + ?assertEqual({4, 4, 6}, {Filter, FilterDocs, FilterAllDocs}), + delete_ddocs(DDocUrl, Rev). +t_view_open_doc_times({_, DbUrl}) -> + {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view), + F = "?filter=_view&view=views/v", + I = "&include_docs=true", + S = "&style=all_docs", + Filter = called_times(fun() -> changes(DbUrl, F) end, DbUrl), + FilterDocs = called_times(fun() -> changes(DbUrl, F ++ I) end, DbUrl), + FilterAllDocs = called_times(fun() -> changes(DbUrl, F ++ S ++ I) end, DbUrl), + ?assertEqual({4, 4, 6}, {Filter, FilterDocs, FilterAllDocs}), + delete_ddocs(DDocUrl, Rev). + +% Utility functions setup_ctx(DbCreateParams) -> Ctx = test_util:start_couch([chttpd]), Hashed = couch_passwords:hash_admin_password(?PASS), @@ -883,6 +926,7 @@ setup_basic() -> CfgKey = "changes_doc_ids_optimization_threshold", ok = config:set("couchdb", CfgKey, "2", _Persist = false), meck:new(couch_changes, [passthrough]), + meck:new(couch_db, [passthrough]), {Ctx, DbUrl}. teardown_basic({Ctx, DbUrl}) -> @@ -891,20 +935,11 @@ teardown_basic({Ctx, DbUrl}) -> teardown_ctx({Ctx, DbUrl}). create_db(Top, Db, Params) -> - case req(put, Top ++ Db ++ Params) of - {201, #{}} -> - ok; - Error -> - error({failed_to_create_test_db, Db, Error}) - end. + {201, #{}} = req(put, Top ++ Db ++ Params), + ok. delete_db(DbUrl) -> - case req(delete, DbUrl) of - {200, #{}} -> - ok; - Error -> - error({failed_to_delete_test_db, DbUrl, Error}) - end. + {200, #{}} = req(delete, DbUrl). doc_fun({Id, Revs, Deleted}) -> Doc = #{ @@ -1033,3 +1068,23 @@ seq(<<_/binary>> = Seq) -> binary_to_integer(NumStr); seq(null) -> null. + +called_times(ReqFun, DbUrl) -> + meck:reset(couch_db), + ReqFun(), + open_doc_calls(DbUrl). + +open_doc_calls(DbUrl) -> + #{path := "/" ++ DbName0} = uri_string:parse(DbUrl), + DbName = ?l2b(DbName0), + FoldFun = + fun([Db, IdOrDocInfo, _Opts], Acc) -> + case {mem3:dbname(couch_db:name(Db)), IdOrDocInfo} of + {DbName, #doc_info{}} -> Acc + 1; + _ -> Acc + end + end, + lists:foldl(FoldFun, 0, meck_history(couch_db, open_doc, 3)). + +meck_history(Mod, Fun, Arity) -> + [A || {_Pid, {_M, F, A}, _R} <- meck:history(Mod), F =:= Fun, length(A) =:= Arity]. diff --git a/src/couch/src/couch_changes.erl b/src/couch/src/couch_changes.erl index e072a2e1ca8..6299cf45139 100644 --- a/src/couch/src/couch_changes.erl +++ b/src/couch/src/couch_changes.erl @@ -19,7 +19,7 @@ wait_updated/3, get_rest_updated/1, configure_filter/4, - filter/3, + filter/5, handle_db_event/3, handle_view_event/3, send_changes_doc_ids/6, @@ -225,44 +225,71 @@ configure_filter(FilterName, Style, Req, Db) -> throw({bad_request, Msg}) end. -filter(Db, #full_doc_info{} = FDI, Filter) -> - filter(Db, couch_doc:to_doc_info(FDI), Filter); -filter(_Db, DocInfo, {default, Style}) -> - apply_style(DocInfo, Style); -filter(_Db, DocInfo, {doc_ids, Style, DocIds}) -> - case lists:member(DocInfo#doc_info.id, DocIds) of - true -> - apply_style(DocInfo, Style); - false -> - [] - end; -filter(Db, DocInfo, {selector, Style, {Selector, _Fields}}) -> - Docs = open_revs(Db, DocInfo, Style), - Passes = [ - mango_selector:match(Selector, couch_doc:to_json_obj(Doc, [])) - || Doc <- Docs - ], - filter_revs(Passes, Docs); -filter(_Db, DocInfo, {design_docs, Style}) -> - case DocInfo#doc_info.id of - <<"_design", _/binary>> -> - apply_style(DocInfo, Style); - _ -> - [] +filter(Db, #full_doc_info{} = FDI, Filter, IncludeDocs, Conflicts) -> + filter(Db, couch_doc:to_doc_info(FDI), Filter, IncludeDocs, Conflicts); +filter(_Db, DocInfo, {default, Style}, _IncludeDocs, _Conflicts) -> + {[], apply_style(DocInfo, Style)}; +filter(_Db, DocInfo, {doc_ids, Style, DocIds}, _IncludeDocs, _Conflicts) -> + Revs = + case lists:member(DocInfo#doc_info.id, DocIds) of + true -> + apply_style(DocInfo, Style); + false -> + [] + end, + {[], Revs}; +filter(_Db, DocInfo, {design_docs, Style}, _IncludeDocs, _Conflicts) -> + Revs = + case DocInfo#doc_info.id of + <<"_design", _/binary>> -> + apply_style(DocInfo, Style); + _ -> + [] + end, + {[], Revs}; +filter(Db, DocInfo, {selector, all_docs, {Selector, _Fields}}, true, true) -> + {DocWin, Docs} = open_all_revs_include_doc(Db, DocInfo), + Passes = [mango_selector:match(Selector, couch_doc:to_json_obj(Doc, [])) || Doc <- Docs], + {DocWin, filter_revs(Passes, Docs)}; +filter(Db, DocInfo, {selector, Style, {Selector, _Fields}}, IncludeDocs, Conflicts) -> + Docs = open_revs(Db, DocInfo, Style, Conflicts), + Passes = [mango_selector:match(Selector, couch_doc:to_json_obj(Doc, [])) || Doc <- Docs], + case IncludeDocs of + true -> {Docs, filter_revs(Passes, Docs)}; + false -> {[], filter_revs(Passes, Docs)} end; -filter(Db, DocInfo, {view, Style, DDoc, VName}) -> - Docs = open_revs(Db, DocInfo, Style), +filter(Db, DocInfo, {view, all_docs, DDoc, VName}, true, true) -> + {DocWin, Docs} = open_all_revs_include_doc(Db, DocInfo), {ok, Passes} = couch_query_servers:filter_view(Db, DDoc, VName, Docs), - filter_revs(Passes, Docs); -filter(Db, DocInfo, {custom, Style, Req0, DDoc, FName}) -> + {DocWin, filter_revs(Passes, Docs)}; +filter(Db, DocInfo, {view, Style, DDoc, VName}, IncludeDocs, Conflicts) -> + Docs = open_revs(Db, DocInfo, Style, Conflicts), + {ok, Passes} = couch_query_servers:filter_view(Db, DDoc, VName, Docs), + case IncludeDocs of + true -> {Docs, filter_revs(Passes, Docs)}; + false -> {[], filter_revs(Passes, Docs)} + end; +filter(Db, DocInfo, {custom, all_docs, Req0, DDoc, FName}, true, true) -> + Req = + case Req0 of + {json_req, _} -> Req0; + #httpd{} -> {json_req, chttpd_external:json_req_obj(Req0, Db)} + end, + {DocWin, Docs} = open_all_revs_include_doc(Db, DocInfo), + {ok, Passes} = couch_query_servers:filter_docs(Req, Db, DDoc, FName, Docs), + {DocWin, filter_revs(Passes, Docs)}; +filter(Db, DocInfo, {custom, Style, Req0, DDoc, FName}, IncludeDocs, Conflicts) -> Req = case Req0 of {json_req, _} -> Req0; #httpd{} -> {json_req, chttpd_external:json_req_obj(Req0, Db)} end, - Docs = open_revs(Db, DocInfo, Style), + Docs = open_revs(Db, DocInfo, Style, Conflicts), {ok, Passes} = couch_query_servers:filter_docs(Req, Db, DDoc, FName, Docs), - filter_revs(Passes, Docs). + case IncludeDocs of + true -> {Docs, filter_revs(Passes, Docs)}; + false -> {[], filter_revs(Passes, Docs)} + end. get_view_qs({json_req, {Props}}) -> {Query} = couch_util:get_value(<<"query">>, Props, {[]}), @@ -357,17 +384,32 @@ apply_style(#doc_info{revs = Revs}, main_only) -> apply_style(#doc_info{revs = Revs}, all_docs) -> [{[{<<"rev">>, couch_doc:rev_to_str(R)}]} || #rev_info{rev = R} <- Revs]. -open_revs(Db, DocInfo, Style) -> +open_revs(Db, DocInfo, Style, Conflicts) -> DocInfos = case Style of main_only -> [DocInfo]; all_docs -> [DocInfo#doc_info{revs = [R]} || R <- DocInfo#doc_info.revs] end, - OpenOpts = [deleted, conflicts], + OpenOpts = + case Conflicts of + true -> [deleted, conflicts]; + false -> [deleted] + end, % Relying on list comprehensions to silence errors OpenResults = [couch_db:open_doc(Db, DI, OpenOpts) || DI <- DocInfos], [Doc || {ok, Doc} <- OpenResults]. +open_all_revs_include_doc(Db, DocInfo) -> + DocInfos = [DocInfo#doc_info{revs = [R]} || R <- DocInfo#doc_info.revs], + OpenOpts = [deleted, conflicts], + OpenResults = [couch_db:open_doc(Db, DI, OpenOpts) || DI <- DocInfos], + Docs = [Doc || {ok, Doc} <- OpenResults], + [Doc1 | RestDocs] = Docs, + RestRevs = [Doc#doc.revs || Doc <- RestDocs, Doc#doc.deleted =:= false], + Conflicts = [{conflicts, [{Pos, RevId} || {Pos, [RevId]} <- RestRevs]}], + Doc2 = Doc1#doc{meta = Conflicts}, + {[Doc2], Docs}. + filter_revs(Passes, Docs) -> lists:flatmap( fun @@ -611,12 +653,14 @@ changes_enumerator(Value, Acc) -> prepend = Prepend, user_acc = UserAcc, limit = Limit, + include_docs = IncludeDocs, + conflicts = Conflicts, resp_type = ResponseType, db = Db, timeout = Timeout, timeout_fun = TimeoutFun } = maybe_upgrade_changes_acc(Acc), - Results0 = filter(Db, Value, Filter), + {_, Results0} = filter(Db, Value, Filter, IncludeDocs, Conflicts), Results = [Result || Result <- Results0, Result /= null], Seq = case Value of diff --git a/src/couch/test/eunit/couch_changes_tests.erl b/src/couch/test/eunit/couch_changes_tests.erl index 59d564e91c6..bbeeac26ae5 100644 --- a/src/couch/test/eunit/couch_changes_tests.erl +++ b/src/couch/test/eunit/couch_changes_tests.erl @@ -16,6 +16,43 @@ -include_lib("couch/include/couch_db.hrl"). -define(TIMEOUT, 6000). +-define(ViewDDoc, + couch_doc:from_json_obj( + {[ + {<<"_id">>, <<"_design/app">>}, + {<<"language">>, <<"javascript">>}, + {<<"views">>, + {[ + {<<"valid">>, + {[ + {<<"map">>, << + "function(doc) {" + " if (doc._id == 'doc1') {" + " emit(doc); " + "} }" + >>} + ]}} + ]}} + ]} + ) +). +-define(CustomDDoc, + couch_doc:from_json_obj( + {[ + {<<"_id">>, <<"_design/app">>}, + {<<"language">>, <<"javascript">>}, + {<<"filters">>, + {[ + {<<"valid">>, << + "function(doc) {" + " if (doc._id == 'doc1') {" + " return true; " + "} }" + >>} + ]}} + ]} + ) +). -record(row, { id, @@ -79,7 +116,6 @@ changes_test_() -> filter_by_custom_function(), filter_by_filter_function(), filter_by_view(), - style_and_include_docs(), style_and_include_docs_with_revtree() ] } @@ -186,22 +222,6 @@ continuous_feed() -> } }. -style_and_include_docs() -> - { - "Style and include_docs", - { - foreach, - fun setup/0, - fun teardown/1, - [ - ?TDEF_FE(t_style_main_only), - ?TDEF_FE(t_style_main_only_with_include_docs), - ?TDEF_FE(t_style_all_docs), - ?TDEF_FE(t_style_all_docs_with_include_docs) - ] - } - }. - style_and_include_docs_with_revtree() -> { "Style and include_docs with revtree", @@ -211,9 +231,39 @@ style_and_include_docs_with_revtree() -> fun teardown/1, [ ?TDEF_FE(t_style_main_only_with_revtree), - ?TDEF_FE(t_style_main_only_with_include_docs_with_revtree), ?TDEF_FE(t_style_all_docs_with_revtree), - ?TDEF_FE(t_style_all_docs_with_include_docs_with_revtree) + ?TDEF_FE(t_style_main_only_with_include_docs_with_revtree), + ?TDEF_FE(t_style_all_docs_with_include_docs_with_revtree), + ?TDEF_FE(t_style_main_only_with_include_docs_conflicts_with_revtree), + ?TDEF_FE(t_style_all_docs_with_include_docs_conflicts_with_revtree), + + ?TDEF_FE(t_doc_ids_style_main_only_with_revtree), + ?TDEF_FE(t_doc_ids_style_all_docs_with_revtree), + ?TDEF_FE(t_doc_ids_style_main_only_with_include_docs_with_revtree), + ?TDEF_FE(t_doc_ids_style_all_docs_with_include_docs_with_revtree), + ?TDEF_FE(t_doc_ids_style_main_only_with_include_docs_conflicts_with_revtree), + ?TDEF_FE(t_doc_ids_style_all_docs_with_include_docs_conflicts_with_revtree), + + ?TDEF_FE(t_selector_style_main_only_with_revtree), + ?TDEF_FE(t_selector_style_all_docs_with_revtree), + ?TDEF_FE(t_selector_style_main_only_with_include_docs_with_revtree), + ?TDEF_FE(t_selector_style_all_docs_with_include_docs_with_revtree), + ?TDEF_FE(t_selector_style_main_only_with_include_docs_conflicts_with_revtree), + ?TDEF_FE(t_selector_style_all_docs_with_include_docs_conflicts_with_revtree), + + ?TDEF_FE(t_view_style_main_only_with_revtree), + ?TDEF_FE(t_view_style_all_docs_with_revtree), + ?TDEF_FE(t_view_style_main_only_with_include_docs_with_revtree), + ?TDEF_FE(t_view_style_all_docs_with_include_docs_with_revtree), + ?TDEF_FE(t_view_style_main_only_with_include_docs_conflicts_with_revtree), + ?TDEF_FE(t_view_style_all_docs_with_include_docs_conflicts_with_revtree), + + ?TDEF_FE(t_custom_style_main_only_with_revtree), + ?TDEF_FE(t_custom_style_all_docs_with_revtree), + ?TDEF_FE(t_custom_style_main_only_with_include_docs_with_revtree), + ?TDEF_FE(t_custom_style_all_docs_with_include_docs_with_revtree), + ?TDEF_FE(t_custom_style_main_only_with_include_docs_conflicts_with_revtree), + ?TDEF_FE(t_custom_style_all_docs_with_include_docs_conflicts_with_revtree) ] } }. @@ -538,10 +588,9 @@ t_receive_heartbeats(_) -> ?assert(Heartbeats3 > Heartbeats2). t_filter_by_doc_attribute({DbName, _}) -> - DDocId = <<"_design/app">>, DDoc = couch_doc:from_json_obj( {[ - {<<"_id">>, DDocId}, + {<<"_id">>, <<"_design/app">>}, {<<"language">>, <<"javascript">>}, {<<"filters">>, {[ @@ -647,120 +696,105 @@ t_filter_by_erlang_view({DbName, _}) -> ?assertMatch([#row{seq = 6, id = <<"doc3">>}], Rows), ?assertEqual(UpSeq, LastSeq). -t_style_main_only({DbName, _}) -> +t_style_main_only_with_revtree({DbName, _}) -> ChArgs = #changes_args{style = main_only}, Req = {json_req, null}, {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), - ?assertEqual(9, length(Rows)), - ?assertEqual(UpSeq, LastSeq), - ?assertMatch( + ?assertEqual(2, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( [ - #row{seq = 1, id = <<"doc1">>}, - #row{seq = 2, id = <<"doc2">>}, - #row{seq = 4, id = <<"doc4">>}, - #row{seq = 5, id = <<"doc5">>}, - #row{seq = 6, id = <<"doc3">>}, - #row{seq = 7, id = <<"doc6">>}, - #row{seq = 8, id = <<"_design/foo">>}, - #row{seq = 9, id = <<"doc7">>}, - #row{seq = 10, id = <<"doc8">>} + #row{ + seq = 3, + id = <<"doc1">>, + doc = nil, + revs = [<<"2-y">>] + }, + #row{ + seq = 4, + id = <<"doc2">>, + deleted = true, + doc = nil, + revs = [<<"1-m">>] + } ], Rows ). -t_style_main_only_with_include_docs({DbName, Revs}) -> - ChArgs = #changes_args{style = main_only, include_docs = true}, - Req = {json_req, null}, - {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), - ?assertEqual(9, length(Rows)), - ?assertEqual(UpSeq, LastSeq), - FirstRev = element(1, Revs), - [FirstRow | _] = Rows, - ?assertMatch( - #row{ - seq = 1, - id = <<"doc1">>, - doc = {[{<<"_id">>, <<"doc1">>}, {<<"_rev">>, FirstRev}]} - }, - FirstRow - ). - -t_style_all_docs({DbName, _}) -> +t_style_all_docs_with_revtree({DbName, _}) -> ChArgs = #changes_args{style = all_docs}, Req = {json_req, null}, {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), - ?assertEqual(9, length(Rows)), - ?assertEqual(UpSeq, LastSeq), - ?assertMatch( + ?assertEqual(2, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( [ - #row{seq = 1, id = <<"doc1">>}, - #row{seq = 2, id = <<"doc2">>}, - #row{seq = 4, id = <<"doc4">>}, - #row{seq = 5, id = <<"doc5">>}, - #row{seq = 6, id = <<"doc3">>}, - #row{seq = 7, id = <<"doc6">>}, - #row{seq = 8, id = <<"_design/foo">>}, - #row{seq = 9, id = <<"doc7">>}, - #row{seq = 10, id = <<"doc8">>} + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + }, + #row{ + seq = 4, + id = <<"doc2">>, + deleted = true, + revs = [<<"1-m">>, <<"1-l">>] + } ], Rows ). -t_style_all_docs_with_include_docs({DbName, Revs}) -> - ChArgs = #changes_args{style = all_docs, include_docs = true}, - Req = {json_req, null}, - {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), - ?assertEqual(9, length(Rows)), - ?assertEqual(UpSeq, LastSeq), - FirstRev = element(1, Revs), - [FirstRow | _] = Rows, - ?assertMatch( - #row{ - seq = 1, - id = <<"doc1">>, - doc = {[{<<"_id">>, <<"doc1">>}, {<<"_rev">>, FirstRev}]} - }, - FirstRow - ). - -t_style_main_only_with_revtree({DbName, _}) -> - ChArgs = #changes_args{style = main_only}, +t_style_main_only_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + include_docs = true + }, Req = {json_req, null}, {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), ?assertEqual(2, length(Rows)), ?assertEqual(4, LastSeq), ?assertEqual(4, UpSeq), - ?assertMatch( + ?assertEqual( [ #row{ seq = 3, id = <<"doc1">>, - doc = nil, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, revs = [<<"2-y">>] }, #row{ seq = 4, id = <<"doc2">>, deleted = true, - doc = nil, + doc = + {[ + {<<"_id">>, <<"doc2">>}, + {<<"_rev">>, <<"1-m">>}, + {<<"_deleted">>, true} + ]}, revs = [<<"1-m">>] } ], Rows ). -t_style_main_only_with_include_docs_with_revtree({DbName, _}) -> +t_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> ChArgs = #changes_args{ - style = main_only, - include_docs = true, - conflicts = true + style = all_docs, + include_docs = true }, Req = {json_req, null}, {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), ?assertEqual(2, length(Rows)), ?assertEqual(4, LastSeq), ?assertEqual(4, UpSeq), - ?assertMatch( + ?assertEqual( [ #row{ seq = 3, @@ -768,10 +802,9 @@ t_style_main_only_with_include_docs_with_revtree({DbName, _}) -> doc = {[ {<<"_id">>, <<"doc1">>}, - {<<"_rev">>, <<"2-y">>}, - {<<"_conflicts">>, [<<"2-x">>]} + {<<"_rev">>, <<"2-y">>} ]}, - revs = [<<"2-y">>] + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] }, #row{ seq = 4, @@ -783,37 +816,53 @@ t_style_main_only_with_include_docs_with_revtree({DbName, _}) -> {<<"_rev">>, <<"1-m">>}, {<<"_deleted">>, true} ]}, - revs = [<<"1-m">>] + revs = [<<"1-m">>, <<"1-l">>] } ], Rows ). -t_style_all_docs_with_revtree({DbName, _}) -> - ChArgs = #changes_args{style = all_docs}, +t_style_main_only_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + include_docs = true, + conflicts = true + }, Req = {json_req, null}, {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), ?assertEqual(2, length(Rows)), - ?assertEqual(4, UpSeq), ?assertEqual(4, LastSeq), - ?assertMatch( + ?assertEqual(4, UpSeq), + ?assertEqual( [ #row{ seq = 3, id = <<"doc1">>, - revs = [<<"2-y">>, <<"2-x">>] + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>] }, #row{ seq = 4, id = <<"doc2">>, deleted = true, - revs = [<<"1-m">>, <<"1-l">>] + doc = + {[ + {<<"_id">>, <<"doc2">>}, + {<<"_rev">>, <<"1-m">>}, + {<<"_deleted">>, true} + ]}, + revs = [<<"1-m">>] } ], Rows ). -t_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> +t_style_all_docs_with_include_docs_conflicts_with_revtree({DbName, _}) -> ChArgs = #changes_args{ style = all_docs, include_docs = true, @@ -824,7 +873,7 @@ t_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> ?assertEqual(2, length(Rows)), ?assertEqual(4, LastSeq), ?assertEqual(4, UpSeq), - ?assertMatch( + ?assertEqual( [ #row{ seq = 3, @@ -835,7 +884,7 @@ t_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> {<<"_rev">>, <<"2-y">>}, {<<"_conflicts">>, [<<"2-x">>]} ]}, - revs = [<<"2-y">>, <<"2-x">>] + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] }, #row{ seq = 4, @@ -853,6 +902,653 @@ t_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> Rows ). +t_doc_ids_style_main_only_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_doc_ids" + }, + DocIds = [<<"doc1">>, <<"doc3">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_doc_ids_style_all_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_doc_ids" + }, + DocIds = [<<"doc1">>, <<"doc3">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_doc_ids_style_main_only_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_doc_ids", + include_docs = true + }, + DocIds = [<<"doc1">>, <<"doc3">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_doc_ids_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_doc_ids", + include_docs = true + }, + DocIds = [<<"doc1">>, <<"doc3">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_doc_ids_style_main_only_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_doc_ids", + include_docs = true, + conflicts = true + }, + DocIds = [<<"doc1">>, <<"doc3">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_doc_ids_style_all_docs_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_doc_ids", + include_docs = true, + conflicts = true + }, + DocIds = [<<"doc1">>, <<"doc3">>, <<"doc9999">>], + Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_selector_style_main_only_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_selector" + }, + LteDoc1 = {[{<<"$lte">>, <<"doc1">>}]}, + Selector = {[{<<"_id">>, LteDoc1}]}, + Req = {json_req, {[{<<"selector">>, Selector}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = nil, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_selector_style_all_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_selector" + }, + LteDoc1 = {[{<<"$lte">>, <<"doc1">>}]}, + Selector = {[{<<"_id">>, LteDoc1}]}, + Req = {json_req, {[{<<"selector">>, Selector}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_selector_style_main_only_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_selector", + include_docs = true + }, + LteDoc1 = {[{<<"$lte">>, <<"doc1">>}]}, + Selector = {[{<<"_id">>, LteDoc1}]}, + Req = {json_req, {[{<<"selector">>, Selector}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_selector_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_selector", + include_docs = true + }, + LteDoc1 = {[{<<"$lte">>, <<"doc1">>}]}, + Selector = {[{<<"_id">>, LteDoc1}]}, + Req = {json_req, {[{<<"selector">>, Selector}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_selector_style_main_only_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_selector", + include_docs = true, + conflicts = true + }, + LteDoc1 = {[{<<"$lte">>, <<"doc1">>}]}, + Selector = {[{<<"_id">>, LteDoc1}]}, + Req = {json_req, {[{<<"selector">>, Selector}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, LastSeq), + ?assertEqual(4, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_selector_style_all_docs_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_selector", + include_docs = true, + conflicts = true + }, + LteDoc1 = {[{<<"$lte">>, <<"doc1">>}]}, + Selector = {[{<<"_id">>, LteDoc1}]}, + Req = {json_req, {[{<<"selector">>, Selector}]}}, + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(4, UpSeq), + ?assertEqual(4, LastSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_view_style_main_only_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_view" + }, + Req = {json_req, {[{<<"query">>, {[{<<"view">>, <<"app/valid">>}]}}]}}, + ok = update_ddoc(DbName, ?ViewDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_view_style_all_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_view" + }, + Req = {json_req, {[{<<"query">>, {[{<<"view">>, <<"app/valid">>}]}}]}}, + ok = update_ddoc(DbName, ?ViewDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_view_style_main_only_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_view", + include_docs = true + }, + Req = {json_req, {[{<<"query">>, {[{<<"view">>, <<"app/valid">>}]}}]}}, + ok = update_ddoc(DbName, ?ViewDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_view_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_view", + include_docs = true + }, + Req = {json_req, {[{<<"query">>, {[{<<"view">>, <<"app/valid">>}]}}]}}, + ok = update_ddoc(DbName, ?ViewDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_view_style_main_only_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "_view", + include_docs = true, + conflicts = true + }, + Req = {json_req, {[{<<"query">>, {[{<<"view">>, <<"app/valid">>}]}}]}}, + ok = update_ddoc(DbName, ?ViewDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_view_style_all_docs_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "_view", + include_docs = true, + conflicts = true + }, + Req = {json_req, {[{<<"query">>, {[{<<"view">>, <<"app/valid">>}]}}]}}, + ok = update_ddoc(DbName, ?ViewDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_custom_style_main_only_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "app/valid" + }, + Req = {json_req, null}, + ok = update_ddoc(DbName, ?CustomDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_custom_style_all_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "app/valid" + }, + Req = {json_req, null}, + ok = update_ddoc(DbName, ?CustomDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_custom_style_main_only_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "app/valid", + include_docs = true + }, + Req = {json_req, null}, + ok = update_ddoc(DbName, ?CustomDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_custom_style_all_docs_with_include_docs_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "app/valid", + include_docs = true + }, + Req = {json_req, null}, + ok = update_ddoc(DbName, ?CustomDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + +t_custom_style_main_only_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = main_only, + filter = "app/valid", + include_docs = true, + conflicts = true + }, + Req = {json_req, null}, + ok = update_ddoc(DbName, ?CustomDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>] + } + ], + Rows + ). + +t_custom_style_all_docs_with_include_docs_conflicts_with_revtree({DbName, _}) -> + ChArgs = #changes_args{ + style = all_docs, + filter = "app/valid", + include_docs = true, + conflicts = true + }, + Req = {json_req, null}, + ok = update_ddoc(DbName, ?CustomDDoc), + {Rows, LastSeq, UpSeq} = run_changes_query(DbName, ChArgs, Req), + ?assertEqual(1, length(Rows)), + ?assertEqual(5, LastSeq), + ?assertEqual(5, UpSeq), + ?assertEqual( + [ + #row{ + seq = 3, + id = <<"doc1">>, + doc = + {[ + {<<"_id">>, <<"doc1">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + revs = [<<"2-y">>, <<"2-x">>, <<"2-d">>] + } + ], + Rows + ). + %%%%%%%%%%%%%%%%%%%% Utility Functions %%%%%%%%%%%%%%%%%%%% update_ddoc(DbName, DDoc) -> {ok, Db} = couch_db:open_int(DbName, [?ADMIN_CTX]), @@ -1110,6 +1806,7 @@ setup_with_revtree() -> {ok, Db1} = couch_db:reopen(Db), update_replicated(Db1, [ doc(<<"doc1">>, [<<"y">>, <<"z">>]), + doc(<<"doc1">>, [<<"d">>, <<"z">>], true), doc(<<"doc2">>, [<<"m">>], true) ]), ok = couch_db:close(Db1), diff --git a/src/fabric/src/fabric_rpc.erl b/src/fabric/src/fabric_rpc.erl index d01f1f5a749..dab69ef4d2c 100644 --- a/src/fabric/src/fabric_rpc.erl +++ b/src/fabric/src/fabric_rpc.erl @@ -546,44 +546,57 @@ changes_enumerator(DocInfo, Acc) -> pending = Pending, epochs = Epochs } = Acc, - #doc_info{id = Id, high_seq = Seq, revs = [#rev_info{deleted = Del} | _]} = DocInfo, - case [X || X <- couch_changes:filter(Db, DocInfo, Filter), X /= null] of - [] -> - ChangesRow = + Opts = + case Conflicts of + true -> [conflicts | DocOptions]; + false -> DocOptions + end, + Seq = DocInfo#doc_info.high_seq, + {Docs0, Revs} = couch_changes:filter(Db, DocInfo, Filter, IncludeDocs, Conflicts), + Changes = [X || X <- Revs, X /= null], + Docs = [X || X <- Docs0, X /= null], + ChangesRow = + case {Changes, Docs, IncludeDocs} of + {[], _, _} -> {no_pass, [ {pending, Pending - 1}, {seq, {Seq, uuid(Db), couch_db:owner_of(Epochs, Seq)}} ]}; - Results -> - Opts = - if - Conflicts -> [conflicts | DocOptions]; - true -> DocOptions - end, - ChangesRow = - {change, [ - {pending, Pending - 1}, - {seq, {Seq, uuid(Db), couch_db:owner_of(Epochs, Seq)}}, - {id, Id}, - {changes, Results}, - {deleted, Del} - | if - IncludeDocs -> [doc_member(Db, DocInfo, Opts, Filter)]; - true -> [] - end - ]} - end, + {_, _, false} -> + changes_row(Changes, [], DocInfo, Acc); + {_, [], true} -> + Docs1 = [doc_member(Db, DocInfo, Opts, Filter)], + changes_row(Changes, Docs1, DocInfo, Acc); + {_, [Doc | _], true} -> + Docs1 = [get_json_doc(Doc, Opts, Filter)], + changes_row(Changes, Docs1, DocInfo, Acc) + end, ok = rexi:stream2(ChangesRow), {ok, Acc#fabric_changes_acc{seq = Seq, pending = Pending - 1}}. +changes_row(Changes, Docs, DocInfo, Acc) -> + #fabric_changes_acc{db = Db, pending = Pending, epochs = Epochs} = Acc, + #doc_info{id = Id, high_seq = Seq, revs = [#rev_info{deleted = Del} | _]} = DocInfo, + {change, [ + {pending, Pending - 1}, + {seq, {Seq, uuid(Db), couch_db:owner_of(Epochs, Seq)}}, + {id, Id}, + {changes, Changes}, + {deleted, Del} + | Docs + ]}. + doc_member(Shard, DocInfo, Opts, Filter) -> case couch_db:open_doc(Shard, DocInfo, [deleted | Opts]) of {ok, Doc} -> - {doc, maybe_filtered_json_doc(Doc, Opts, Filter)}; + get_json_doc(Doc, Opts, Filter); Error -> Error end. +get_json_doc(Doc, Opts, Filter) -> + {doc, maybe_filtered_json_doc(Doc, Opts, Filter)}. + maybe_filtered_json_doc(Doc, Opts, {selector, _Style, {_Selector, Fields}}) when Fields =/= nil -> diff --git a/src/fabric/test/eunit/fabric_changes_test.erl b/src/fabric/test/eunit/fabric_changes_test.erl new file mode 100644 index 00000000000..9b850696dfe --- /dev/null +++ b/src/fabric/test/eunit/fabric_changes_test.erl @@ -0,0 +1,558 @@ +% 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(fabric_changes_test). + +-include_lib("couch/include/couch_db.hrl"). +-include_lib("couch/include/couch_eunit.hrl"). + +-define(View, + {<<"views">>, + {[{<<"v">>, {[{<<"map">>, <<"function(doc) { if (doc._id == 'a') { emit(doc); } }">>}]}}]}} +). +-define(Custom, {<<"filters">>, {[{<<"f">>, <<"function(doc) { return (doc._id == 'a'); }">>}]}}). + +fabric_changes_test_() -> + { + setup, + fun setup/0, + fun teardown/1, + with([ + ?TDEF(t_main_only), + ?TDEF(t_all_docs), + ?TDEF(t_main_only_include_docs), + ?TDEF(t_all_docs_include_docs), + ?TDEF(t_main_only_include_docs_conflicts), + ?TDEF(t_all_docs_include_docs_conflicts) + ]) + }. + +changes_selector_test_() -> + { + setup, + fun setup/0, + fun teardown/1, + with([ + ?TDEF(t_selector_main_only), + ?TDEF(t_selector_all_docs), + ?TDEF(t_selector_main_only_include_docs), + ?TDEF(t_selector_all_docs_include_docs), + ?TDEF(t_selector_main_only_include_docs_conflicts), + ?TDEF(t_selector_all_docs_include_docs_conflicts) + ]) + }. + +changes_view_test_() -> + { + setup, + fun() -> setup_ddoc(?View) end, + fun teardown/1, + with([ + ?TDEF(t_view_main_only), + ?TDEF(t_view_all_docs), + ?TDEF(t_view_main_only_include_docs), + ?TDEF(t_view_all_docs_include_docs), + ?TDEF(t_view_main_only_include_docs_conflicts), + ?TDEF(t_view_all_docs_include_docs_conflicts) + ]) + }. + +changes_custom_test_() -> + { + setup, + fun() -> setup_ddoc(?Custom) end, + fun teardown/1, + with([ + ?TDEF(t_custom_main_only), + ?TDEF(t_custom_all_docs), + ?TDEF(t_custom_main_only_include_docs), + ?TDEF(t_custom_all_docs_include_docs), + ?TDEF(t_custom_main_only_include_docs_conflicts), + ?TDEF(t_custom_all_docs_include_docs_conflicts) + ]) + }. + +t_main_only({_, DbName}) -> + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{style = main_only}), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes). + +t_all_docs({_, DbName}) -> + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{style = all_docs}), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ). + +t_main_only_include_docs({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + include_docs = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_all_docs_include_docs({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + include_docs = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_main_only_include_docs_conflicts({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + include_docs = true, + conflicts = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_all_docs_include_docs_conflicts({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + include_docs = true, + conflicts = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_selector_main_only({_, DbName}) -> + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "_selector", + filter_fun = {selector, main_only, {{[{<<"_id">>, {[{<<"$eq">>, <<"a">>}]}}]}, nil}} + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes). + +t_selector_all_docs({_, DbName}) -> + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "_selector", + filter_fun = {selector, all_docs, {{[{<<"_id">>, {[{<<"$eq">>, <<"a">>}]}}]}, nil}} + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ). + +t_selector_main_only_include_docs({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "_selector", + filter_fun = {selector, main_only, {{[{<<"_id">>, {[{<<"$eq">>, <<"a">>}]}}]}, nil}}, + include_docs = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_selector_all_docs_include_docs({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "_selector", + filter_fun = {selector, all_docs, {{[{<<"_id">>, {[{<<"$eq">>, <<"a">>}]}}]}, nil}}, + include_docs = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_selector_main_only_include_docs_conflicts({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "_selector", + filter_fun = {selector, main_only, {{[{<<"_id">>, {[{<<"$eq">>, <<"a">>}]}}]}, nil}}, + include_docs = true, + conflicts = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_selector_all_docs_include_docs_conflicts({_, DbName}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "_selector", + filter_fun = {selector, all_docs, {{[{<<"_id">>, {[{<<"$eq">>, <<"a">>}]}}]}, nil}}, + include_docs = true, + conflicts = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_view_main_only({_, DbName, Rev}) -> + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "_view", + filter_fun = {fetch, view, main_only, {<<"_design/ddoc">>, Rev}, <<"v">>} + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes). + +t_view_all_docs({_, DbName, Rev}) -> + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "_view", + filter_fun = {fetch, view, all_docs, {<<"_design/ddoc">>, Rev}, <<"v">>} + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ). + +t_view_main_only_include_docs({_, DbName, Rev}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "_view", + filter_fun = {fetch, view, main_only, {<<"_design/ddoc">>, Rev}, <<"v">>}, + include_docs = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_view_all_docs_include_docs({_, DbName, Rev}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "_view", + filter_fun = {fetch, view, all_docs, {<<"_design/ddoc">>, Rev}, <<"v">>}, + include_docs = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_view_main_only_include_docs_conflicts({_, DbName, Rev}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "_view", + filter_fun = {fetch, view, main_only, {<<"_design/ddoc">>, Rev}, <<"v">>}, + include_docs = true, + conflicts = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_view_all_docs_include_docs_conflicts({_, DbName, Rev}) -> + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "_view", + filter_fun = {fetch, view, all_docs, {<<"_design/ddoc">>, Rev}, <<"v">>}, + include_docs = true, + conflicts = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_custom_main_only({_, DbName, Rev}) -> + Req = {json_req, null}, + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "ddoc/f", + filter_fun = {fetch, custom, main_only, Req, {<<"_design/ddoc">>, Rev}, <<"f">>} + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes). + +t_custom_all_docs({_, DbName, Rev}) -> + Req = {json_req, null}, + {ok, [#{changes := Changes}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "ddoc/f", + filter_fun = {fetch, custom, all_docs, Req, {<<"_design/ddoc">>, Rev}, <<"f">>} + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ). + +t_custom_main_only_include_docs({_, DbName, Rev}) -> + Req = {json_req, null}, + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "ddoc/f", + filter_fun = {fetch, custom, main_only, Req, {<<"_design/ddoc">>, Rev}, <<"f">>}, + include_docs = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_custom_all_docs_include_docs({_, DbName, Rev}) -> + Req = {json_req, null}, + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "ddoc/f", + filter_fun = {fetch, custom, all_docs, Req, {<<"_design/ddoc">>, Rev}, <<"f">>}, + include_docs = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>} + ]}, + Doc + ). + +t_custom_main_only_include_docs_conflicts({_, DbName, Rev}) -> + Req = {json_req, null}, + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = main_only, + filter = "ddoc/f", + filter_fun = {fetch, custom, main_only, Req, {<<"_design/ddoc">>, Rev}, <<"f">>}, + include_docs = true, + conflicts = true + }), + ?assertEqual([{[{<<"rev">>, <<"2-y">>}]}], Changes), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +t_custom_all_docs_include_docs_conflicts({_, DbName, Rev}) -> + Req = {json_req, null}, + {ok, [#{changes := Changes, doc := Doc}], _, _} = + changes(DbName, #changes_args{ + style = all_docs, + filter = "ddoc/f", + filter_fun = {fetch, custom, all_docs, Req, {<<"_design/ddoc">>, Rev}, <<"f">>}, + include_docs = true, + conflicts = true + }), + ?assertEqual( + [ + {[{<<"rev">>, <<"2-y">>}]}, + {[{<<"rev">>, <<"2-x">>}]}, + {[{<<"rev">>, <<"2-d">>}]} + ], + Changes + ), + ?assertEqual( + {[ + {<<"_id">>, <<"a">>}, + {<<"_rev">>, <<"2-y">>}, + {<<"_conflicts">>, [<<"2-x">>]} + ]}, + Doc + ). + +%%%%%%%%%%%%%%%%%%%% Utility Functions %%%%%%%%%%%%%%%%%%%% +setup() -> + Ctx = test_util:start_couch([fabric]), + DbName = ?tempdb(), + ok = fabric:create_db(DbName, [{q, 1}, {n, 1}]), + Docs = [ + #doc{id = <<"a">>, revs = {1, [<<"z">>]}}, + #doc{id = <<"a">>, revs = {2, [<<"x">>, <<"z">>]}}, + #doc{id = <<"a">>, revs = {2, [<<"y">>, <<"z">>]}}, + #doc{id = <<"a">>, revs = {2, [<<"d">>, <<"z">>]}, deleted = true} + ], + Opts = [?REPLICATED_CHANGES], + {ok, []} = fabric:update_docs(DbName, Docs, Opts), + {Ctx, DbName}. + +setup_ddoc(Ddoc) -> + {Ctx, DbName} = setup(), + Doc = #doc{ + id = <<"_design/ddoc">>, + revs = {0, []}, + body = {[{<<"language">>, <<"javascript">>}, Ddoc]} + }, + {ok, Rev} = fabric:update_doc(DbName, Doc, [?ADMIN_CTX]), + {Ctx, DbName, Rev}. + +teardown({Ctx, DbName, _}) -> + teardown({Ctx, DbName}); +teardown({Ctx, DbName}) -> + ok = fabric:delete_db(DbName, [?ADMIN_CTX]), + test_util:stop_couch(Ctx). + +changes_callback(start, Acc) -> + {ok, Acc}; +changes_callback({change, {Change}}, Acc) -> + CM = maps:from_list(Change), + {ok, [CM | Acc]}; +changes_callback({stop, EndSeq, Pending}, Acc) -> + {ok, Acc, EndSeq, Pending}. + +changes(DbName, #changes_args{} = Args) -> + fabric_util:isolate(fun() -> fabric:changes(DbName, fun changes_callback/2, [], Args) end).