You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2018/03/16 20:00:05 UTC

[couchdb] 11/20: WIP - add test suite

This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch COUCHDB-3326-clustered-purge-davisp-refactor
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 425a60ddbe98f9880d3e62b591a792848d144830
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Wed Mar 14 14:09:16 2018 -0500

    WIP - add test suite
---
 src/couch/src/test_engine_compaction.erl       | 116 +++++++-
 src/couch/src/test_engine_fold_purged_docs.erl | 133 +++++++++
 src/couch/src/test_engine_get_set_props.erl    |   3 +-
 src/couch/src/test_engine_purge_docs.erl       |  35 ++-
 src/couch/src/test_engine_util.erl             | 162 +++++++----
 src/couch/test/couch_db_purge_docs_tests.erl   | 360 +++++++++++++++++++++++++
 6 files changed, 744 insertions(+), 65 deletions(-)

diff --git a/src/couch/src/test_engine_compaction.erl b/src/couch/src/test_engine_compaction.erl
index 09a1e4e..51970ee 100644
--- a/src/couch/src/test_engine_compaction.erl
+++ b/src/couch/src/test_engine_compaction.erl
@@ -84,10 +84,8 @@ cet_compact_with_everything() ->
     BarRev = test_engine_util:prev_rev(BarFDI),
 
     Actions3 = [
-        {batch, [
-            {purge, {<<"foo">>, FooRev#rev_info.rev}},
-            {purge, {<<"bar">>, BarRev#rev_info.rev}}
-        ]}
+        {purge, {<<"foo">>, FooRev#rev_info.rev}},
+        {purge, {<<"bar">>, BarRev#rev_info.rev}}
     ],
 
     {ok, St6} = test_engine_util:apply_actions(Engine, St5, Actions3),
@@ -97,7 +95,8 @@ cet_compact_with_everything() ->
         {<<"foo">>, [FooRev#rev_info.rev]}
     ],
 
-    ?assertEqual(PurgedIdRevs, lists:sort(Engine:get_last_purged(St6))),
+    {ok, PIdRevs6} = Engine:fold_purged_docs(St6, 0, fun fold_fun/2, [], []),
+    ?assertEqual(PurgedIdRevs, PIdRevs6),
 
     {ok, St7} = try
         [Att0, Att1, Att2, Att3, Att4] = test_engine_util:prep_atts(Engine, St6, [
@@ -131,6 +130,9 @@ cet_compact_with_everything() ->
     end),
 
     {ok, St10, undefined} = Engine:finish_compaction(St9, DbName, [], Term),
+    {ok, PIdRevs11} = Engine:fold_purged_docs(St10, 0, fun fold_fun/2, [], []),
+    ?assertEqual(PurgedIdRevs, PIdRevs11),
+
     Db2 = test_engine_util:db_as_term(Engine, St10),
     Diff = test_engine_util:term_diff(Db1, Db2),
     ?assertEqual(nodiff, Diff).
@@ -175,6 +177,106 @@ cet_recompact_updates() ->
     ?assertEqual(nodiff, Diff).
 
 
+cet_recompact_purge() ->
+    {ok, Engine, Path, St1} = test_engine_util:init_engine(dbpath),
+
+    Actions1 = [
+        {create, {<<"foo">>, []}},
+        {create, {<<"bar">>, []}},
+        {conflict, {<<"bar">>, [{<<"vsn">>, 2}]}},
+        {create, {<<"baz">>, []}}
+    ],
+
+    {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+    {ok, St3, DbName, _, Term} = test_engine_util:compact(Engine, St2, Path),
+
+    [BarFDI, BazFDI] = Engine:open_docs(St3, [<<"bar">>, <<"baz">>]),
+    BarRev = test_engine_util:prev_rev(BarFDI),
+    BazRev = test_engine_util:prev_rev(BazFDI),
+    Actions2 = [
+        {purge, {<<"bar">>, BarRev#rev_info.rev}},
+        {purge, {<<"baz">>, BazRev#rev_info.rev}}
+    ],
+    {ok, St4} = test_engine_util:apply_actions(Engine, St3, Actions2),
+    Db1 = test_engine_util:db_as_term(Engine, St4),
+
+    {ok, St5, NewPid} = Engine:finish_compaction(St4, DbName, [], Term),
+
+    ?assertEqual(true, is_pid(NewPid)),
+    Ref = erlang:monitor(process, NewPid),
+
+    NewTerm = receive
+        {'$gen_cast', {compact_done, Engine, Term0}} ->
+            Term0;
+        {'DOWN', Ref, _, _, Reason} ->
+            erlang:error({compactor_died, Reason});
+        {'$gen_call', {NewPid, Ref2}, get_disposable_purge_seq} ->
+            NewPid!{Ref2, {ok, 0}},
+            receive
+                {'$gen_cast', {compact_done, Engine, Term0}} ->
+                    Term0;
+                {'DOWN', Ref, _, _, Reason} ->
+                    erlang:error({compactor_died, Reason})
+                after 10000 ->
+                    erlang:error(compactor_timed_out)
+            end
+        after 10000 ->
+            erlang:error(compactor_timed_out)
+    end,
+
+    {ok, St6, undefined} = Engine:finish_compaction(St5, DbName, [], NewTerm),
+    Db2 = test_engine_util:db_as_term(Engine, St6),
+    Diff = test_engine_util:term_diff(Db1, Db2),
+    ?assertEqual(nodiff, Diff).
+
+
+% temporary ignoring this test as it times out
+ignore_cet_compact_purged_docs_limit() ->
+    {ok, Engine, Path, St1} = test_engine_util:init_engine(dbpath),
+    % create NumDocs docs
+    NumDocs = 1200,
+    {RActions, RIds} = lists:foldl(fun(Id, {CActions, CIds}) ->
+        Id1 = docid(Id),
+        Action = {create, {Id1, [{<<"int">>, Id}]}},
+        {[Action| CActions], [Id1| CIds]}
+    end, {[], []}, lists:seq(1, NumDocs)),
+    Ids = lists:reverse(RIds),
+    {ok, St2} = test_engine_util:apply_actions(Engine, St1,
+        lists:reverse(RActions)),
+
+    % purge NumDocs docs
+    FDIs = Engine:open_docs(St2, Ids),
+    RevActions2 = lists:foldl(fun(FDI, CActions) ->
+        Id = FDI#full_doc_info.id,
+        PrevRev = test_engine_util:prev_rev(FDI),
+        Rev = PrevRev#rev_info.rev,
+        [{purge, {Id, Rev}}| CActions]
+    end, [], FDIs),
+    {ok, St3} = test_engine_util:apply_actions(Engine, St2,
+        lists:reverse(RevActions2)),
+
+    % check that before compaction all NumDocs of purge_requests
+    % are in purge_tree,
+    % even if NumDocs=1200 is greater than purged_docs_limit=1000
+    {ok, PurgedIdRevs} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
+    ?assertEqual(1, Engine:get_oldest_purge_seq(St3)),
+    ?assertEqual(NumDocs, length(PurgedIdRevs)),
+
+    % compact db
+    {ok, St4, DbName, _, Term} = test_engine_util:compact(Engine, St3, Path),
+    {ok, St5, undefined} = Engine:finish_compaction(St4, DbName, [], Term),
+
+    % check that after compaction only purged_docs_limit purge_requests
+    % are in purge_tree
+    PurgedDocsLimit = Engine:get_purged_docs_limit(St5),
+    OldestPSeq = Engine:get_oldest_purge_seq(St5),
+    {ok, PurgedIdRevs2} = Engine:fold_purged_docs(
+        St5, OldestPSeq - 1, fun fold_fun/2, [], []),
+    ExpectedOldestPSeq = NumDocs - PurgedDocsLimit + 1,
+    ?assertEqual(ExpectedOldestPSeq, OldestPSeq),
+    ?assertEqual(PurgedDocsLimit, length(PurgedIdRevs2)).
+
+
 docid(I) ->
     Str = io_lib:format("~4..0b", [I]),
     iolist_to_binary(Str).
@@ -183,3 +285,7 @@ docid(I) ->
 local_docid(I) ->
     Str = io_lib:format("_local/~4..0b", [I]),
     iolist_to_binary(Str).
+
+
+fold_fun({_PSeq, _UUID, Id, Revs}, Acc) ->
+    [{Id, Revs} | Acc].
diff --git a/src/couch/src/test_engine_fold_purged_docs.erl b/src/couch/src/test_engine_fold_purged_docs.erl
new file mode 100644
index 0000000..1dc0885
--- /dev/null
+++ b/src/couch/src/test_engine_fold_purged_docs.erl
@@ -0,0 +1,133 @@
+% 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(test_engine_fold_purged_docs).
+-compile(export_all).
+
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+
+-define(NUM_DOCS, 100).
+
+
+cet_empty_purged_docs() ->
+    {ok, Engine, St} = test_engine_util:init_engine(),
+    ?assertEqual({ok, []}, Engine:fold_purged_docs(St, 0, fun fold_fun/2, [], [])).
+
+
+cet_all_purged_docs() ->
+    {ok, Engine, St1} = test_engine_util:init_engine(),
+
+    {RActions, RIds} = lists:foldl(fun(Id, {CActions, CIds}) ->
+        Id1 = docid(Id),
+        Action = {create, {Id1, [{<<"int">>, Id}]}},
+        {[Action| CActions], [Id1| CIds]}
+     end, {[], []}, lists:seq(1, ?NUM_DOCS)),
+    Actions = lists:reverse(RActions),
+    Ids = lists:reverse(RIds),
+    {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions),
+
+    FDIs = Engine:open_docs(St2, Ids),
+    {RevActions2, RevIdRevs} = lists:foldl(fun(FDI, {CActions, CIdRevs}) ->
+        Id = FDI#full_doc_info.id,
+        PrevRev = test_engine_util:prev_rev(FDI),
+        Rev = PrevRev#rev_info.rev,
+        Action = {purge, {Id, Rev}},
+        {[Action| CActions], [{Id, [Rev]}| CIdRevs]}
+     end, {[], []}, FDIs),
+    {Actions2, IdsRevs} = {lists:reverse(RevActions2), lists:reverse(RevIdRevs)},
+
+    {ok, St3} = test_engine_util:apply_actions(Engine, St2, Actions2),
+    {ok, PurgedIdRevs} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
+    ?assertEqual(IdsRevs, lists:reverse(PurgedIdRevs)).
+
+
+cet_start_seq() ->
+    {ok, Engine, St1} = test_engine_util:init_engine(),
+    Actions1 = [
+        {create, {docid(1), [{<<"int">>, 1}]}},
+        {create, {docid(2), [{<<"int">>, 2}]}},
+        {create, {docid(3), [{<<"int">>, 3}]}},
+        {create, {docid(4), [{<<"int">>, 4}]}},
+        {create, {docid(5), [{<<"int">>, 5}]}}
+    ],
+    Ids = [docid(1), docid(2), docid(3), docid(4), docid(5)],
+    {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+
+    FDIs = Engine:open_docs(St2, Ids),
+    {RActions2, RIdRevs} = lists:foldl(fun(FDI, {CActions, CIdRevs}) ->
+        Id = FDI#full_doc_info.id,
+        PrevRev = test_engine_util:prev_rev(FDI),
+        Rev = PrevRev#rev_info.rev,
+        Action = {purge, {Id, Rev}},
+        {[Action| CActions], [{Id, [Rev]}| CIdRevs]}
+    end, {[], []}, FDIs),
+    {ok, St3} = test_engine_util:apply_actions(Engine, St2, lists:reverse(RActions2)),
+
+    StartSeq = 3,
+    StartSeqIdRevs = lists:nthtail(StartSeq, lists:reverse(RIdRevs)),
+    {ok, PurgedIdRevs} = Engine:fold_purged_docs(St3, StartSeq, fun fold_fun/2, [], []),
+    ?assertEqual(StartSeqIdRevs, lists:reverse(PurgedIdRevs)).
+
+
+cet_id_rev_repeated() ->
+    {ok, Engine, St1} = test_engine_util:init_engine(),
+
+    Actions1 = [
+        {create, {<<"foo">>, [{<<"vsn">>, 1}]}},
+        {conflict, {<<"foo">>, [{<<"vsn">>, 2}]}}
+    ],
+    {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+
+    [FDI1] = Engine:open_docs(St2, [<<"foo">>]),
+    PrevRev1 = test_engine_util:prev_rev(FDI1),
+    Rev1 = PrevRev1#rev_info.rev,
+    Actions2 = [
+        {purge, {<<"foo">>, Rev1}}
+    ],
+    {ok, St3} = test_engine_util:apply_actions(Engine, St2, Actions2),
+    PurgedIdRevs0 = [{<<"foo">>, [Rev1]}],
+    {ok, PurgedIdRevs1} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
+    ?assertEqual(PurgedIdRevs0, PurgedIdRevs1),
+    ?assertEqual(1, Engine:get_purge_seq(St3)),
+
+    % purge the same Id,Rev when the doc still exists
+    {ok, St4} = test_engine_util:apply_actions(Engine, St3, Actions2),
+    {ok, PurgedIdRevs2} = Engine:fold_purged_docs(St4, 0, fun fold_fun/2, [], []),
+    ?assertEqual(PurgedIdRevs0, PurgedIdRevs2),
+    ?assertEqual(1, Engine:get_purge_seq(St4)),
+
+    [FDI2] = Engine:open_docs(St4, [<<"foo">>]),
+    PrevRev2 = test_engine_util:prev_rev(FDI2),
+    Rev2 = PrevRev2#rev_info.rev,
+    Actions3 = [
+        {purge, {<<"foo">>, Rev2}}
+    ],
+    {ok, St5} = test_engine_util:apply_actions(Engine, St4, Actions3),
+    PurgedIdRevs00 = [{<<"foo">>, [Rev1]}, {<<"foo">>, [Rev2]}],
+
+    % purge the same Id,Rev when the doc was completely purged
+    {ok, St6} = test_engine_util:apply_actions(Engine, St5, Actions3),
+    {ok, PurgedIdRevs3} = Engine:fold_purged_docs(St6, 0, fun fold_fun/2, [], []),
+    ?assertEqual(PurgedIdRevs00, lists:reverse(PurgedIdRevs3)),
+    ?assertEqual(2, Engine:get_purge_seq(St6)).
+
+
+fold_fun({_PSeq, _UUID, Id, Revs}, Acc) ->
+    [{Id, Revs} | Acc].
+
+
+docid(I) ->
+    Str = io_lib:format("~4..0b", [I]),
+    iolist_to_binary(Str).
diff --git a/src/couch/src/test_engine_get_set_props.erl b/src/couch/src/test_engine_get_set_props.erl
index 6d2a447..ac6aca8 100644
--- a/src/couch/src/test_engine_get_set_props.erl
+++ b/src/couch/src/test_engine_get_set_props.erl
@@ -34,7 +34,8 @@ cet_default_props() ->
     ?assertEqual(true, is_integer(Engine:get_disk_version(St))),
     ?assertEqual(0, Engine:get_update_seq(St)),
     ?assertEqual(0, Engine:get_purge_seq(St)),
-    ?assertEqual([], Engine:get_last_purged(St)),
+    ?assertEqual(true, is_integer(Engine:get_purged_docs_limit(St))),
+    ?assertEqual(true, Engine:get_purged_docs_limit(St) > 0),
     ?assertEqual(dso, Engine:get_security(St)),
     ?assertEqual(1000, Engine:get_revs_limit(St)),
     ?assertMatch(<<_:32/binary>>, Engine:get_uuid(St)),
diff --git a/src/couch/src/test_engine_purge_docs.erl b/src/couch/src/test_engine_purge_docs.erl
index e5bf249..a1dbae7 100644
--- a/src/couch/src/test_engine_purge_docs.erl
+++ b/src/couch/src/test_engine_purge_docs.erl
@@ -25,12 +25,13 @@ cet_purge_simple() ->
         {create, {<<"foo">>, [{<<"vsn">>, 1}]}}
     ],
     {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+    {ok, PIdRevs2} = Engine:fold_purged_docs(St2, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(1, Engine:get_doc_count(St2)),
     ?assertEqual(0, Engine:get_del_doc_count(St2)),
     ?assertEqual(1, Engine:get_update_seq(St2)),
     ?assertEqual(0, Engine:get_purge_seq(St2)),
-    ?assertEqual([], Engine:get_last_purged(St2)),
+    ?assertEqual([], PIdRevs2),
 
     [FDI] = Engine:open_docs(St2, [<<"foo">>]),
     PrevRev = test_engine_util:prev_rev(FDI),
@@ -40,12 +41,13 @@ cet_purge_simple() ->
         {purge, {<<"foo">>, Rev}}
     ],
     {ok, St3} = test_engine_util:apply_actions(Engine, St2, Actions2),
+    {ok, PIdRevs3} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(0, Engine:get_doc_count(St3)),
     ?assertEqual(0, Engine:get_del_doc_count(St3)),
     ?assertEqual(2, Engine:get_update_seq(St3)),
     ?assertEqual(1, Engine:get_purge_seq(St3)),
-    ?assertEqual([{<<"foo">>, [Rev]}], Engine:get_last_purged(St3)).
+    ?assertEqual([{<<"foo">>, [Rev]}], PIdRevs3).
 
 
 cet_purge_conflicts() ->
@@ -56,12 +58,13 @@ cet_purge_conflicts() ->
         {conflict, {<<"foo">>, [{<<"vsn">>, 2}]}}
     ],
     {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+    {ok, PIdRevs2} = Engine:fold_purged_docs(St2, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(1, Engine:get_doc_count(St2)),
     ?assertEqual(0, Engine:get_del_doc_count(St2)),
     ?assertEqual(2, Engine:get_update_seq(St2)),
     ?assertEqual(0, Engine:get_purge_seq(St2)),
-    ?assertEqual([], Engine:get_last_purged(St2)),
+    ?assertEqual([], PIdRevs2),
 
     [FDI1] = Engine:open_docs(St2, [<<"foo">>]),
     PrevRev1 = test_engine_util:prev_rev(FDI1),
@@ -71,12 +74,13 @@ cet_purge_conflicts() ->
         {purge, {<<"foo">>, Rev1}}
     ],
     {ok, St3} = test_engine_util:apply_actions(Engine, St2, Actions2),
+    {ok, PIdRevs3} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(1, Engine:get_doc_count(St3)),
     ?assertEqual(0, Engine:get_del_doc_count(St3)),
-    ?assertEqual(4, Engine:get_update_seq(St3)),
+    ?assertEqual(3, Engine:get_update_seq(St3)),
     ?assertEqual(1, Engine:get_purge_seq(St3)),
-    ?assertEqual([{<<"foo">>, [Rev1]}], Engine:get_last_purged(St3)),
+    ?assertEqual([{<<"foo">>, [Rev1]}], PIdRevs3),
 
     [FDI2] = Engine:open_docs(St3, [<<"foo">>]),
     PrevRev2 = test_engine_util:prev_rev(FDI2),
@@ -86,12 +90,13 @@ cet_purge_conflicts() ->
         {purge, {<<"foo">>, Rev2}}
     ],
     {ok, St4} = test_engine_util:apply_actions(Engine, St3, Actions3),
+    {ok, PIdRevs4} = Engine:fold_purged_docs(St4, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(0, Engine:get_doc_count(St4)),
     ?assertEqual(0, Engine:get_del_doc_count(St4)),
-    ?assertEqual(5, Engine:get_update_seq(St4)),
+    ?assertEqual(4, Engine:get_update_seq(St4)),
     ?assertEqual(2, Engine:get_purge_seq(St4)),
-    ?assertEqual([{<<"foo">>, [Rev2]}], Engine:get_last_purged(St4)).
+    ?assertEqual([{<<"foo">>, [Rev2]}, {<<"foo">>, [Rev1]}], PIdRevs4).
 
 
 cet_add_delete_purge() ->
@@ -103,12 +108,13 @@ cet_add_delete_purge() ->
     ],
 
     {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+    {ok, PIdRevs2} = Engine:fold_purged_docs(St2, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(0, Engine:get_doc_count(St2)),
     ?assertEqual(1, Engine:get_del_doc_count(St2)),
     ?assertEqual(2, Engine:get_update_seq(St2)),
     ?assertEqual(0, Engine:get_purge_seq(St2)),
-    ?assertEqual([], Engine:get_last_purged(St2)),
+    ?assertEqual([], PIdRevs2),
 
     [FDI] = Engine:open_docs(St2, [<<"foo">>]),
     PrevRev = test_engine_util:prev_rev(FDI),
@@ -118,12 +124,13 @@ cet_add_delete_purge() ->
         {purge, {<<"foo">>, Rev}}
     ],
     {ok, St3} = test_engine_util:apply_actions(Engine, St2, Actions2),
+    {ok, PIdRevs3} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(0, Engine:get_doc_count(St3)),
     ?assertEqual(0, Engine:get_del_doc_count(St3)),
     ?assertEqual(3, Engine:get_update_seq(St3)),
     ?assertEqual(1, Engine:get_purge_seq(St3)),
-    ?assertEqual([{<<"foo">>, [Rev]}], Engine:get_last_purged(St3)).
+    ?assertEqual([{<<"foo">>, [Rev]}], PIdRevs3).
 
 
 cet_add_two_purge_one() ->
@@ -135,12 +142,13 @@ cet_add_two_purge_one() ->
     ],
 
     {ok, St2} = test_engine_util:apply_actions(Engine, St1, Actions1),
+    {ok, PIdRevs2} = Engine:fold_purged_docs(St2, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(2, Engine:get_doc_count(St2)),
     ?assertEqual(0, Engine:get_del_doc_count(St2)),
     ?assertEqual(2, Engine:get_update_seq(St2)),
     ?assertEqual(0, Engine:get_purge_seq(St2)),
-    ?assertEqual([], Engine:get_last_purged(St2)),
+    ?assertEqual([], PIdRevs2),
 
     [FDI] = Engine:open_docs(St2, [<<"foo">>]),
     PrevRev = test_engine_util:prev_rev(FDI),
@@ -150,9 +158,14 @@ cet_add_two_purge_one() ->
         {purge, {<<"foo">>, Rev}}
     ],
     {ok, St3} = test_engine_util:apply_actions(Engine, St2, Actions2),
+    {ok, PIdRevs3} = Engine:fold_purged_docs(St3, 0, fun fold_fun/2, [], []),
 
     ?assertEqual(1, Engine:get_doc_count(St3)),
     ?assertEqual(0, Engine:get_del_doc_count(St3)),
     ?assertEqual(3, Engine:get_update_seq(St3)),
     ?assertEqual(1, Engine:get_purge_seq(St3)),
-    ?assertEqual([{<<"foo">>, [Rev]}], Engine:get_last_purged(St3)).
+    ?assertEqual([{<<"foo">>, [Rev]}], PIdRevs3).
+
+
+fold_fun({_Pseq, _UUID, Id, Revs}, Acc) ->
+    [{Id, Revs} | Acc].
\ No newline at end of file
diff --git a/src/couch/src/test_engine_util.erl b/src/couch/src/test_engine_util.erl
index 8999753..bd49969 100644
--- a/src/couch/src/test_engine_util.erl
+++ b/src/couch/src/test_engine_util.erl
@@ -24,6 +24,7 @@
     test_engine_attachments,
     test_engine_fold_docs,
     test_engine_fold_changes,
+    test_engine_fold_purged_docs,
     test_engine_purge_docs,
     test_engine_compaction,
     test_engine_ref_counting
@@ -132,28 +133,35 @@ apply_action(Engine, St, Action) ->
     apply_batch(Engine, St, [Action]).
 
 
+apply_batch(Engine, St, [{purge, {Id, Revs}}]) ->
+    UpdateSeq = Engine:get_update_seq(St) + 1,
+    case gen_write(Engine, St, {purge, {Id, Revs}}, UpdateSeq) of
+        {_, _, purged_before}->
+            St;
+        {Pair, _, {Id, PRevs}} ->
+            UUID = couch_uuids:new(),
+            {ok, NewSt} = Engine:purge_doc_revs(
+                St, [Pair], [{UUID, Id, PRevs}]),
+            NewSt
+    end;
+
 apply_batch(Engine, St, Actions) ->
     UpdateSeq = Engine:get_update_seq(St) + 1,
-    AccIn = {UpdateSeq, [], [], []},
+    AccIn = {UpdateSeq, [], []},
     AccOut = lists:foldl(fun(Action, Acc) ->
-        {SeqAcc, DocAcc, LDocAcc, PurgeAcc} = Acc,
+        {SeqAcc, DocAcc, LDocAcc} = Acc,
         case Action of
             {_, {<<"_local/", _/binary>>, _}} ->
                 LDoc = gen_local_write(Engine, St, Action),
-                {SeqAcc, DocAcc, [LDoc | LDocAcc], PurgeAcc};
+                {SeqAcc, DocAcc, [LDoc | LDocAcc]};
             _ ->
-                case gen_write(Engine, St, Action, SeqAcc) of
-                    {_OldFDI, _NewFDI} = Pair ->
-                        {SeqAcc + 1, [Pair | DocAcc], LDocAcc, PurgeAcc};
-                    {Pair, NewSeqAcc, NewPurgeInfo} ->
-                        NewPurgeAcc = [NewPurgeInfo | PurgeAcc],
-                        {NewSeqAcc, [Pair | DocAcc], LDocAcc, NewPurgeAcc}
-                end
+                {OldFDI, NewFDI} = gen_write(Engine, St, Action, SeqAcc),
+                {SeqAcc + 1, [{OldFDI, NewFDI} | DocAcc], LDocAcc}
         end
     end, AccIn, Actions),
-    {_, Docs0, LDocs, PurgeIdRevs} = AccOut,
+    {_, Docs0, LDocs} = AccOut,
     Docs = lists:reverse(Docs0),
-    {ok, NewSt} = Engine:write_doc_infos(St, Docs, LDocs, PurgeIdRevs),
+    {ok, NewSt} = Engine:write_doc_infos(St, Docs, LDocs),
     NewSt.
 
 
@@ -221,39 +229,71 @@ gen_write(Engine, St, {create, {DocId, Body, Atts0}}, UpdateSeq) ->
     }};
 
 gen_write(Engine, St, {purge, {DocId, PrevRevs0, _}}, UpdateSeq) ->
-    [#full_doc_info{} = PrevFDI] = Engine:open_docs(St, [DocId]),
-    PrevRevs = if is_list(PrevRevs0) -> PrevRevs0; true -> [PrevRevs0] end,
-
-    #full_doc_info{
-        rev_tree = PrevTree
-    } = PrevFDI,
-
-    {NewTree, RemRevs} = couch_key_tree:remove_leafs(PrevTree, PrevRevs),
-    RemovedAll = lists:sort(RemRevs) == lists:sort(PrevRevs),
-    if RemovedAll -> ok; true ->
-        % If we didn't purge all the requested revisions
-        % then its a bug in the test.
-        erlang:error({invalid_purge_test_revs, PrevRevs})
-    end,
+    case Engine:open_docs(St, [DocId]) of
+    [not_found] ->
+        % Check if this doc has been purged before
+        FoldFun = fun({_PSeq, _UUID, Id, _Revs}, _Acc) ->
+            case Id of
+                DocId -> true;
+                _ -> false
+            end
+        end,
+        {ok, IsPurgedBefore} = Engine:fold_purged_docs(
+            St, 0, FoldFun, false, []),
+        case IsPurgedBefore of
+            true -> {{}, UpdateSeq, purged_before};
+            false -> erlang:error({invalid_purge_test_id, DocId})
+        end;
+    [#full_doc_info{} = PrevFDI] ->
+        PrevRevs = if is_list(PrevRevs0) -> PrevRevs0; true -> [PrevRevs0] end,
+
+        #full_doc_info{
+            rev_tree = PrevTree
+        } = PrevFDI,
+
+        {NewTree, RemRevs0} = couch_key_tree:remove_leafs(PrevTree, PrevRevs),
+        {RemRevs, NotRemRevs} = lists:partition(fun(R) ->
+                lists:member(R, RemRevs0) end, PrevRevs),
+
+        if NotRemRevs == [] -> ok; true ->
+            % Check if these Revs have been purged before
+            FoldFun = fun({_Pseq, _UUID, Id, Revs}, Acc) ->
+                case Id of
+                    DocId -> Acc ++ Revs;
+                    _ -> Acc
+                end
+            end,
+            {ok, PurgedRevs} = Engine:fold_purged_docs(St, 0, FoldFun, [], []),
+            case lists:subtract(PrevRevs, PurgedRevs) of [] -> ok; _ ->
+                % If we didn't purge all the requested revisions
+                % and they haven't been purged before
+                % then its a bug in the test.
+                erlang:error({invalid_purge_test_revs, PrevRevs})
+            end
+        end,
+
+        case {RemRevs, NewTree} of
+            {[], _} ->
+                {{PrevFDI, PrevFDI}, UpdateSeq, purged_before};
+            {_, []} ->
+                % We've completely purged the document
+                {{PrevFDI, not_found}, UpdateSeq, {DocId, RemRevs}};
+            _ ->
+                % We have to relabel the update_seq of all
+                % leaves. See couch_db_updater for details.
+                {NewNewTree, NewUpdateSeq} = couch_key_tree:mapfold(fun
+                    (_RevId, Leaf, leaf, InnerSeqAcc) ->
+                        {Leaf#leaf{seq = InnerSeqAcc}, InnerSeqAcc + 1};
+                    (_RevId, Value, _Type, InnerSeqAcc) ->
+                        {Value, InnerSeqAcc}
+                end, UpdateSeq, NewTree),
+                NewFDI = PrevFDI#full_doc_info{
+                    update_seq = NewUpdateSeq - 1,
+                    rev_tree = NewNewTree
+                },
+                {{PrevFDI, NewFDI}, NewUpdateSeq, {DocId, RemRevs}}
 
-    case NewTree of
-        [] ->
-            % We've completely purged the document
-            {{PrevFDI, not_found}, UpdateSeq, {DocId, RemRevs}};
-        _ ->
-            % We have to relabel the update_seq of all
-            % leaves. See couch_db_updater for details.
-            {NewNewTree, NewUpdateSeq} = couch_key_tree:mapfold(fun
-                (_RevId, Leaf, leaf, InnerSeqAcc) ->
-                    {Leaf#leaf{seq = InnerSeqAcc}, InnerSeqAcc + 1};
-                (_RevId, Value, _Type, InnerSeqAcc) ->
-                    {Value, InnerSeqAcc}
-            end, UpdateSeq, NewTree),
-            NewFDI = PrevFDI#full_doc_info{
-                update_seq = NewUpdateSeq - 1,
-                rev_tree = NewNewTree
-            },
-            {{PrevFDI, NewFDI}, NewUpdateSeq, {DocId, RemRevs}}
+        end
     end;
 
 gen_write(Engine, St, {Action, {DocId, Body, Atts0}}, UpdateSeq) ->
@@ -408,7 +448,8 @@ db_as_term(Engine, St) ->
         {props, db_props_as_term(Engine, St)},
         {docs, db_docs_as_term(Engine, St)},
         {local_docs, db_local_docs_as_term(Engine, St)},
-        {changes, db_changes_as_term(Engine, St)}
+        {changes, db_changes_as_term(Engine, St)},
+        {purged_docs, db_purged_docs_as_term(Engine, St)}
     ].
 
 
@@ -419,7 +460,7 @@ db_props_as_term(Engine, St) ->
         get_disk_version,
         get_update_seq,
         get_purge_seq,
-        get_last_purged,
+        get_purged_docs_limit,
         get_security,
         get_revs_limit,
         get_uuid,
@@ -452,6 +493,15 @@ db_changes_as_term(Engine, St) ->
     end, Changes)).
 
 
+db_purged_docs_as_term(Engine, St) ->
+    StartPSeq = Engine:get_oldest_purge_seq(St) - 1,
+    FoldFun = fun({PSeq, UUID, Id, Revs}, Acc) ->
+        [{PSeq, UUID, Id, Revs} | Acc]
+    end,
+    {ok, PDocs} = Engine:fold_purged_docs(St, StartPSeq, FoldFun, [], []),
+    lists:reverse(PDocs).
+
+
 fdi_to_term(Engine, St, FDI) ->
     #full_doc_info{
         id = DocId,
@@ -577,8 +627,24 @@ compact(Engine, St1, DbPath) ->
     Term = receive
         {'$gen_cast', {compact_done, Engine, Term0}} ->
             Term0;
-        {'DOWN', Ref, _, _, Reason} ->
-            erlang:error({compactor_died, Reason})
+            erlang:error({compactor_died, Reason});
+        {'$gen_call', {Pid, Ref2}, get_disposable_purge_seq} ->
+            % assuming no client exists (no internal replications or indexes)
+            PSeq = Engine:get_purge_seq(St2),
+            OldestPSeq = Engine:get_oldest_purge_seq(St2),
+            PDocsLimit = Engine:get_purged_docs_limit(St2),
+            ExpectedDispPSeq = PSeq - PDocsLimit,
+            DisposablePSeq = if ExpectedDispPSeq > 0 -> ExpectedDispPSeq;
+                    true -> OldestPSeq - 1 end,
+            Pid!{Ref2, {ok, DisposablePSeq}},
+            receive
+                {'$gen_cast', {compact_done, Engine, Term0}} ->
+                    Term0;
+                {'DOWN', Ref, _, _, Reason} ->
+                    erlang:error({compactor_died, Reason})
+                after 10000 ->
+                    erlang:error(compactor_timed_out)
+            end
         after ?COMPACTOR_TIMEOUT ->
             erlang:error(compactor_timed_out)
     end,
diff --git a/src/couch/test/couch_db_purge_docs_tests.erl b/src/couch/test/couch_db_purge_docs_tests.erl
new file mode 100644
index 0000000..1608957
--- /dev/null
+++ b/src/couch/test/couch_db_purge_docs_tests.erl
@@ -0,0 +1,360 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_db_purge_docs_tests).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+
+setup() ->
+    DbName = ?tempdb(),
+    {ok, _Db} = create_db(DbName),
+    DbName.
+
+teardown(DbName) ->
+    delete_db(DbName),
+    ok.
+
+couch_db_purge_docs_test_() ->
+    {
+        "Couch_db purge_docs",
+        [
+            {
+                setup,
+                fun test_util:start_couch/0, fun test_util:stop_couch/1,
+                [couch_db_purge_docs()]
+            },
+            purge_with_replication()
+        ]
+
+    }.
+
+
+couch_db_purge_docs() ->
+    {
+       foreach,
+            fun setup/0, fun teardown/1,
+            [
+                fun purge_simple/1,
+                fun add_delete_purge/1,
+                fun add_two_purge_one/1,
+                fun purge_id_not_exist/1,
+                fun purge_non_leaf_rev/1,
+                fun purge_conflicts/1,
+                fun purge_deep_tree/1
+            ]
+    }.
+
+
+purge_simple(DbName) ->
+    ?_test(
+        begin
+            {ok, Db} = couch_db:open_int(DbName, []),
+            Doc1 = {[{<<"_id">>, <<"foo1">>}, {<<"vsn">>, 1.1}]},
+            Doc2 = {[{<<"_id">>, <<"foo2">>}, {<<"vsn">>, 1.2}]},
+            {ok, Rev} = save_doc(Db, Doc1),
+            {ok, Rev2} = save_doc(Db, Doc2),
+            couch_db:ensure_full_commit(Db),
+
+            {ok, Db2} = couch_db:reopen(Db),
+            ?assertEqual(2, couch_db_engine:get_doc_count(Db2)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db2)),
+            ?assertEqual(2, couch_db_engine:get_update_seq(Db2)),
+            ?assertEqual(0, couch_db_engine:get_purge_seq(Db2)),
+
+            UUID = couch_uuids:new(), UUID2 = couch_uuids:new(),
+            {ok, [{ok, PRevs}, {ok, PRevs2}]} = couch_db:purge_docs(
+                Db2, [{UUID, <<"foo1">>, [Rev]}, {UUID2, <<"foo2">>, [Rev2]}]
+            ),
+
+            ?assertEqual([Rev], PRevs),
+            ?assertEqual([Rev2], PRevs2),
+
+            {ok, Db3} = couch_db:reopen(Db2),
+            {ok, PIdsRevs} = couch_db:fold_purged_docs(
+                Db3, 0, fun fold_fun/2, [], []),
+            ?assertEqual(0, couch_db_engine:get_doc_count(Db3)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db3)),
+            ?assertEqual(3, couch_db_engine:get_update_seq(Db3)),
+            ?assertEqual(2, couch_db_engine:get_purge_seq(Db3)),
+            ?assertEqual([{<<"foo2">>, [Rev2]}, {<<"foo1">>, [Rev]}], PIdsRevs)
+        end).
+
+
+add_delete_purge(DbName) ->
+    ?_test(
+        begin
+            {ok, Db0} = couch_db:open_int(DbName, []),
+            Doc0 = {[{<<"_id">>,<<"foo">>}, {<<"vsn">>, 1}]},
+            {ok, Rev} = save_doc(Db0, Doc0),
+            couch_db:ensure_full_commit(Db0),
+            {ok, Db1} = couch_db:reopen(Db0),
+
+            Doc1 = {[
+                {<<"_id">>, <<"foo">>}, {<<"vsn">>, 2},
+                {<<"_rev">>, couch_doc:rev_to_str(Rev)},
+                {<<"_deleted">>, true}]
+            },
+            {ok, Rev2} = save_doc(Db1, Doc1),
+            couch_db:ensure_full_commit(Db1),
+
+            {ok, Db2} = couch_db:reopen(Db1),
+            {ok, PIdsRevs1} = couch_db:fold_purged_docs(
+                Db2, 0, fun fold_fun/2, [], []),
+            ?assertEqual(0, couch_db_engine:get_doc_count(Db2)),
+            ?assertEqual(1, couch_db_engine:get_del_doc_count(Db2)),
+            ?assertEqual(2, couch_db_engine:get_update_seq(Db2)),
+            ?assertEqual(0, couch_db_engine:get_purge_seq(Db2)),
+            ?assertEqual([], PIdsRevs1),
+
+            UUID = couch_uuids:new(),
+            {ok, [{ok, PRevs}]} = couch_db:purge_docs(
+                Db2, [{UUID, <<"foo">>, [Rev2]}]),
+            ?assertEqual([Rev2], PRevs),
+
+            {ok, Db3} = couch_db:reopen(Db2),
+            {ok, PIdsRevs2} = couch_db:fold_purged_docs(
+                Db3, 0, fun fold_fun/2, [], []),
+            ?assertEqual(0, couch_db_engine:get_doc_count(Db3)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db3)),
+            ?assertEqual(3, couch_db_engine:get_update_seq(Db3)),
+            ?assertEqual(1, couch_db_engine:get_purge_seq(Db3)),
+            ?assertEqual([{<<"foo">>, [Rev2]}], PIdsRevs2)
+        end).
+
+
+add_two_purge_one(DbName) ->
+    ?_test(
+        begin
+            {ok, Db} = couch_db:open_int(DbName, []),
+            Doc1 = {[{<<"_id">>, <<"foo1">>}, {<<"vsn">>, 1}]},
+            Doc2 = {[{<<"_id">>, <<"foo2">>}, {<<"vsn">>, 2}]},
+            {ok, Rev} = save_doc(Db, Doc1),
+            {ok, _Rev2} = save_doc(Db, Doc2),
+            couch_db:ensure_full_commit(Db),
+
+            {ok, Db2} = couch_db:reopen(Db),
+            ?assertEqual(2, couch_db_engine:get_doc_count(Db2)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db2)),
+            ?assertEqual(2, couch_db_engine:get_update_seq(Db2)),
+            ?assertEqual(0, couch_db_engine:get_purge_seq(Db2)),
+
+            UUID = couch_uuids:new(),
+            {ok, [{ok, PRevs}]} = couch_db:purge_docs(Db2,
+                [{UUID, <<"foo1">>, [Rev]}]),
+            ?assertEqual([Rev], PRevs),
+
+            {ok, Db3} = couch_db:reopen(Db2),
+            {ok, PIdsRevs} = couch_db:fold_purged_docs(
+                Db3, 0, fun fold_fun/2, [], []),
+            ?assertEqual(1, couch_db_engine:get_doc_count(Db3)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db3)),
+            ?assertEqual(3, couch_db_engine:get_update_seq(Db3)),
+            ?assertEqual(1, couch_db_engine:get_purge_seq(Db3)),
+            ?assertEqual([{<<"foo1">>, [Rev]}], PIdsRevs)
+        end).
+
+
+purge_id_not_exist(DbName) ->
+    ?_test(
+        begin
+            {ok, Db} = couch_db:open_int(DbName, []),
+            UUID = couch_uuids:new(),
+            {ok, [{ok, PRevs}]} = couch_db:purge_docs(Db,
+                [{UUID, <<"foo">>, [{0, <<0>>}]}]),
+            ?assertEqual([], PRevs),
+
+            {ok, Db2} = couch_db:reopen(Db),
+            {ok, PIdsRevs} = couch_db:fold_purged_docs(
+                Db2, 0, fun fold_fun/2, [], []),
+            ?assertEqual(0, couch_db_engine:get_doc_count(Db2)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db2)),
+            ?assertEqual(0, couch_db_engine:get_update_seq(Db2)),
+            ?assertEqual(0, couch_db_engine:get_purge_seq(Db2)),
+            ?assertEqual([], PIdsRevs)
+        end).
+
+
+purge_non_leaf_rev(DbName) ->
+    ?_test(
+        begin
+            {ok, Db} = couch_db:open_int(DbName, []),
+            Doc0 = {[{<<"_id">>, <<"foo">>}, {<<"vsn">>, 1}]},
+            {ok, Rev} = save_doc(Db, Doc0),
+            couch_db:ensure_full_commit(Db),
+            {ok, Db2} = couch_db:reopen(Db),
+
+            Doc1 = {[
+                {<<"_id">>, <<"foo">>}, {<<"vsn">>, 2},
+                {<<"_rev">>, couch_doc:rev_to_str(Rev)}
+            ]},
+            {ok, _Rev2} = save_doc(Db2, Doc1),
+            couch_db:ensure_full_commit(Db2),
+            {ok, Db3} = couch_db:reopen(Db2),
+
+            UUID = couch_uuids:new(),
+            {ok, [{ok, PRevs}]} = couch_db:purge_docs(Db3,
+                [{UUID, <<"foo">>, [Rev]}]),
+            ?assertEqual([], PRevs),
+
+            {ok, Db4} = couch_db:reopen(Db3),
+            {ok, PIdsRevs} = couch_db:fold_purged_docs(Db4, 0, fun fold_fun/2, [], []),
+            ?assertEqual(1, couch_db_engine:get_doc_count(Db4)),
+            ?assertEqual(2, couch_db_engine:get_update_seq(Db4)),
+            ?assertEqual(0, couch_db_engine:get_purge_seq(Db4)),
+            ?assertEqual([], PIdsRevs)
+        end).
+
+
+purge_conflicts(DbName) ->
+    ?_test(
+        begin
+            {ok, Db} = couch_db:open_int(DbName, []),
+            Doc = {[{<<"_id">>, <<"foo">>}, {<<"vsn">>, <<"v1.1">>}]},
+            {ok, Rev} = save_doc(Db, Doc),
+            couch_db:ensure_full_commit(Db),
+            {ok, Db2} = couch_db:reopen(Db),
+
+            % create a conflict
+            DocConflict = #doc{
+                id = <<"foo">>,
+                revs = {1, [crypto:hash(md5, <<"v1.2">>)]},
+                body = {[ {<<"vsn">>,  <<"v1.2">>}]}
+            },
+            {ok, _} = couch_db:update_doc(Db2, DocConflict, [], replicated_changes),
+            couch_db:ensure_full_commit(Db2),
+            {ok, Db3} = couch_db:reopen(Db2),
+
+            UUID = couch_uuids:new(),
+            {ok, [{ok, PRevs}]} = couch_db:purge_docs(Db3,
+                [{UUID, <<"foo">>, [Rev]}]),
+            ?assertEqual([Rev], PRevs),
+
+            {ok, Db4} = couch_db:reopen(Db3),
+            {ok, PIdsRevs} = couch_db:fold_purged_docs(
+                Db4, 0, fun fold_fun/2, [], []),
+            % still has one doc
+            ?assertEqual(1, couch_db_engine:get_doc_count(Db4)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db4)),
+            ?assertEqual(3, couch_db_engine:get_update_seq(Db4)),
+            ?assertEqual(1, couch_db_engine:get_purge_seq(Db4)),
+            ?assertEqual([{<<"foo">>, [Rev]}], PIdsRevs)
+        end).
+
+
+purge_deep_tree(DbName) ->
+    ?_test(
+        begin
+            NRevs = 100,
+            {ok, Db0} = couch_db:open_int(DbName, []),
+            Doc0 = {[{<<"_id">>, <<"bar">>}, {<<"vsn">>, 0}]},
+            {ok, InitRev} = save_doc(Db0, Doc0),
+            ok = couch_db:close(Db0),
+            LastRev = lists:foldl(fun(V, PrevRev) ->
+                {ok, Db} = couch_db:open_int(DbName, []),
+                {ok, Rev} = save_doc(Db,
+                    {[{<<"_id">>, <<"bar">>},
+                    {<<"vsn">>, V},
+                    {<<"_rev">>, couch_doc:rev_to_str(PrevRev)}]}
+                ),
+                ok = couch_db:close(Db),
+                Rev
+            end, InitRev, lists:seq(2, NRevs)),
+            {ok, Db1} = couch_db:open_int(DbName, []),
+
+            % purge doc
+            UUID = couch_uuids:new(),
+            {ok, [{ok, PRevs}]} = couch_db:purge_docs(Db1,
+                [{UUID, <<"bar">>, [LastRev]}]),
+            ?assertEqual([LastRev], PRevs),
+
+            {ok, Db2} = couch_db:reopen(Db1),
+            % no docs left
+            ?assertEqual(0, couch_db_engine:get_doc_count(Db2)),
+            ?assertEqual(0, couch_db_engine:get_del_doc_count(Db2)),
+            ?assertEqual(1, couch_db_engine:get_purge_seq(Db2)),
+            ?assertEqual(NRevs + 1 , couch_db_engine:get_update_seq(Db2))
+        end).
+
+
+purge_with_replication() ->
+    ?_test(
+        begin
+            Ctx = test_util:start_couch([couch_replicator]),
+            Source = ?tempdb(),
+            {ok, SourceDb} = create_db(Source),
+            Target = ?tempdb(),
+            {ok, _Db} = create_db(Target),
+
+            % create Doc and do replication to Target
+            {ok, Rev} = save_doc(SourceDb,
+                {[{<<"_id">>, <<"foo">>}, {<<"vsn">>, 1}]}),
+            couch_db:ensure_full_commit(SourceDb),
+            {ok, SourceDb2} = couch_db:reopen(SourceDb),
+            RepObject = {[
+                {<<"source">>, Source},
+                {<<"target">>, Target}
+            ]},
+            {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+            {ok, TargetDb} = couch_db:open_int(Target, []),
+            {ok, Doc} = couch_db:get_doc_info(TargetDb, <<"foo">>),
+
+            % purge Doc on Source and do replication to Target
+            % assert purges don't get replicated to Target
+            UUID = couch_uuids:new(),
+            {ok, _} = couch_db:purge_docs(SourceDb2, [{UUID, <<"foo">>, [Rev]}]),
+            {ok, SourceDb3} = couch_db:reopen(SourceDb2),
+            {ok, _} = couch_replicator:replicate(RepObject, ?ADMIN_USER),
+            {ok, TargetDb2} = couch_db:open_int(Target, []),
+            {ok, Doc2} = couch_db:get_doc_info(TargetDb2, <<"foo">>),
+            [Rev2] = Doc2#doc_info.revs,
+            ?assertEqual(Rev, Rev2#rev_info.rev),
+            ?assertEqual(Doc, Doc2),
+            ?assertEqual(0, couch_db_engine:get_doc_count(SourceDb3)),
+            ?assertEqual(1, couch_db_engine:get_purge_seq(SourceDb3)),
+            ?assertEqual(1, couch_db_engine:get_doc_count(TargetDb2)),
+            ?assertEqual(0, couch_db_engine:get_purge_seq(TargetDb2)),
+
+            % replicate from Target to Source
+            % assert that Doc reappears on Source
+            RepObject2 = {[
+                {<<"source">>, Target},
+                {<<"target">>, Source}
+            ]},
+            {ok, _} = couch_replicator:replicate(RepObject2, ?ADMIN_USER),
+            {ok, SourceDb4} = couch_db:reopen(SourceDb3),
+            {ok, Doc3} = couch_db:get_doc_info(SourceDb4, <<"foo">>),
+            [Rev3] = Doc3#doc_info.revs,
+            ?assertEqual(Rev, Rev3#rev_info.rev),
+            ?assertEqual(1, couch_db_engine:get_doc_count(SourceDb4)),
+            ?assertEqual(1, couch_db_engine:get_purge_seq(SourceDb4)),
+
+            delete_db(Source),
+            delete_db(Target),
+            ok = application:stop(couch_replicator),
+            ok = test_util:stop_couch(Ctx)
+        end).
+
+
+create_db(DbName) ->
+    couch_db:create(DbName, [?ADMIN_CTX, overwrite]).
+
+delete_db(DbName) ->
+    couch_server:delete(DbName, [?ADMIN_CTX]).
+
+save_doc(Db, Json) ->
+    Doc = couch_doc:from_json_obj(Json),
+    couch_db:update_doc(Db, Doc, []).
+
+fold_fun({_PSeq, _UUID, Id, Revs}, Acc) ->
+    [{Id, Revs} | Acc].
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
davisp@apache.org.