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 2019/05/01 18:19:27 UTC

[couchdb] branch prototype/rfc-001-revision-metadata-model updated: Moar tests

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

davisp pushed a commit to branch prototype/rfc-001-revision-metadata-model
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/prototype/rfc-001-revision-metadata-model by this push:
     new 104d19d  Moar tests
104d19d is described below

commit 104d19d4a83ab978af4c7537abe57f9b41812445
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Wed May 1 13:19:10 2019 -0500

    Moar tests
---
 src/fabric/src/fabric2.hrl                  |   5 -
 src/fabric/src/fabric2_db.erl               |   2 +-
 src/fabric/src/fabric2_fdb.erl              | 226 ++++++++++++++++++++--------
 src/fabric/src/fabric2_util.erl             |  48 ++++++
 src/fabric/test/fabric2_db_misc_tests.erl   |   2 +-
 src/fabric/test/fabric2_doc_count_tests.erl |   6 +-
 src/fabric/test/fabric2_doc_fold_tests.erl  | 209 +++++++++++++++++++++++++
 7 files changed, 423 insertions(+), 75 deletions(-)

diff --git a/src/fabric/src/fabric2.hrl b/src/fabric/src/fabric2.hrl
index 451c779..e8d0b13 100644
--- a/src/fabric/src/fabric2.hrl
+++ b/src/fabric/src/fabric2.hrl
@@ -54,8 +54,3 @@
 -define(PDICT_TX_ID_KEY, '$fabric_tx_id').
 -define(PDICT_TX_RES_KEY, '$fabric_tx_result').
 -define(COMMIT_UNKNOWN_RESULT, 1021).
-
-
-% Various utility macros
-
--define(UNSET_VS, {versionstamp, 16#FFFFFFFFFFFFFFFF, 16#FFFF}).
diff --git a/src/fabric/src/fabric2_db.erl b/src/fabric/src/fabric2_db.erl
index 680372e..9971bbc 100644
--- a/src/fabric/src/fabric2_db.erl
+++ b/src/fabric/src/fabric2_db.erl
@@ -309,7 +309,7 @@ get_update_seq(#{} = Db) ->
     fabric2_fdb:transactional(Db, fun(TxDb) ->
         Opts = [{limit, 1}, {reverse, true}],
         case fabric2_fdb:get_changes(TxDb, Opts) of
-            [] -> fabric2_util:to_hex(<<0:80>>);
+            [] -> fabric2_util:to_hex(fabric2_util:seq_zero());
             [{Seq, _}] -> Seq
         end
     end).
diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index b0f85fe..19b19bb 100644
--- a/src/fabric/src/fabric2_fdb.erl
+++ b/src/fabric/src/fabric2_fdb.erl
@@ -248,9 +248,10 @@ get_info(#{} = Db) ->
 
     RawSeq = case erlfdb:wait(ChangesFuture) of
         [] ->
-            <<0:80>>;
+            fabric2_util:seq_zero();
         [{SeqKey, _}] ->
-            {?DB_CHANGES, SeqBin} = erlfdb_tuple:unpack(SeqKey, DbPrefix),
+            {?DB_CHANGES, SeqVS} = erlfdb_tuple:unpack(SeqKey, DbPrefix),
+            <<51:8, SeqBin:13/binary>> = erlfdb_tuple:pack({SeqVS}),
             SeqBin
     end,
     CProp = {update_seq, fabric2_util:to_hex(RawSeq)},
@@ -425,18 +426,18 @@ write_doc(#{} = Db0, Doc, NewWinner0, OldWinner, ToUpdate, ToRemove) ->
     NewWinner = NewWinner0#{winner := true},
     NewRevId = maps:get(rev_id, NewWinner),
 
-    {WKey, WVal} = revinfo_to_fdb(DbPrefix, DocId, NewWinner),
+    {WKey, WVal, WinnerVS} = revinfo_to_fdb(Tx, DbPrefix, DocId, NewWinner),
     ok = erlfdb:set_versionstamped_value(Tx, WKey, WVal),
 
     lists:foreach(fun(RI0) ->
         RI = RI0#{winner := false},
-        {K, V} = revinfo_to_fdb(DbPrefix, DocId, RI),
+        {K, V, undefined} = revinfo_to_fdb(Tx, DbPrefix, DocId, RI),
         ok = erlfdb:set(Tx, K, V)
     end, ToUpdate),
 
     lists:foreach(fun(RI0) ->
         RI = RI0#{winner := false},
-        {K, _} = revinfo_to_fdb(DbPrefix, DocId, RI),
+        {K, _, undefined} = revinfo_to_fdb(Tx, DbPrefix, DocId, RI),
         ok = erlfdb:clear(Tx, K)
     end, ToRemove),
 
@@ -473,7 +474,7 @@ write_doc(#{} = Db0, Doc, NewWinner0, OldWinner, ToUpdate, ToRemove) ->
         erlfdb:clear(Tx, OldSeqKey)
     end,
 
-    NewSeqKey = erlfdb_tuple:pack_vs({?DB_CHANGES, ?UNSET_VS}, DbPrefix),
+    NewSeqKey = erlfdb_tuple:pack_vs({?DB_CHANGES, WinnerVS}, DbPrefix),
     NewSeqVal = erlfdb_tuple:pack({DocId, Deleted, NewRevId}),
     erlfdb:set_versionstamped_key(Tx, NewSeqKey, NewSeqVal),
 
@@ -549,77 +550,95 @@ write_doc_body(#{} = Db0, #doc{} = Doc) ->
     erlfdb:set(Tx, NewDocKey, NewDocVal).
 
 
-fold_docs(#{} = Db, UserFun, UserAcc0, _Options) ->
+fold_docs(#{} = Db, UserFun, UserAcc0, Options) ->
     #{
         tx := Tx,
         db_prefix := DbPrefix
     } = ensure_current(Db),
 
-    DocCountKey = erlfdb_tuple:pack({?DB_STATS, <<"doc_count">>}, DbPrefix),
-    DocCountFuture = erlfdb:get(Tx, DocCountKey),
-
-    {Start, End} = erlfdb_tuple:range({?DB_ALL_DOCS}, DbPrefix),
-    RangeFuture = erlfdb:get_range(Tx, Start, End),
-
-    DocCount = ?bin2uint(erlfdb:wait(DocCountFuture)),
-
-    {ok, UserAcc1} = UserFun({meta, [
-        {total, DocCount},
-        {offset, null}
-    ]}, UserAcc0),
-
-    UserAcc2 = lists:foldl(fun({K, V}, Acc) ->
-        {?DB_ALL_DOCS, DocId} = erlfdb_tuple:unpack(K, DbPrefix),
-        RevId = erlfdb_tuple:unpack(V),
-
-        {ok, NewAcc} = UserFun({row, [
-            {id, DocId},
-            {key, DocId},
-            {value, couch_doc:rev_to_str(RevId)}
-        ]}, Acc),
+    {Reverse, Start, End} = get_dir_and_bounds(DbPrefix, Options),
 
-        NewAcc
-    end, UserAcc1, erlfdb:wait(RangeFuture)),
+    DocCountKey = erlfdb_tuple:pack({?DB_STATS, <<"doc_count">>}, DbPrefix),
+    DocCountBin = erlfdb:wait(erlfdb:get(Tx, DocCountKey)),
 
-    UserFun(complete, UserAcc2).
+    try
+        UserAcc1 = maybe_stop(UserFun({meta, [
+            {total, ?bin2uint(DocCountBin)},
+            {offset, null}
+        ]}, UserAcc0)),
+
+        UserAcc2 = erlfdb:fold_range(Tx, Start, End, fun({K, V}, UserAccIn) ->
+            {?DB_ALL_DOCS, DocId} = erlfdb_tuple:unpack(K, DbPrefix),
+            RevId = erlfdb_tuple:unpack(V),
+            maybe_stop(UserFun({row, [
+                {id, DocId},
+                {key, DocId},
+                {value, couch_doc:rev_to_str(RevId)}
+            ]}, UserAccIn))
+        end, UserAcc1, [{reverse, Reverse}]),
+
+        {ok, maybe_stop(UserFun(complete, UserAcc2))}
+    catch throw:{stop, FinalUserAcc} ->
+        {ok, FinalUserAcc}
+    end.
 
 
-fold_changes(#{} = Db, SinceSeq, UserFun, UserAcc0, _Options) ->
+fold_changes(#{} = Db, SinceSeq0, UserFun, UserAcc0, Options) ->
     #{
         tx := Tx,
         db_prefix := DbPrefix
     } = ensure_current(Db),
 
-    {Start0, End} = erlfdb_tuple:range({?DB_CHANGES}, DbPrefix),
-    Start = erlang:max(Start0, SinceSeq),
-    Future = erlfdb:get_range(Tx, Start, End),
+    SinceSeq1 = fabric2_util:from_hex(SinceSeq0),
+    SinceSeq2 = <<51:8, SinceSeq1/binary>>,
+
+    Reverse = case fabric2_util:get_value(dir, Options, fwd) of
+        fwd -> false;
+        rev -> true
+    end,
 
-    {ok, UserAcc1} = UserFun(start, UserAcc0),
+    {Start0, End0} = case Reverse of
+        false ->
+            {{?DB_CHANGES, SinceSeq2}, {?DB_CHANGES, <<16#FF>>}};
+        true ->
+            {{?DB_CHANGES}, {?DB_CHANGES, SinceSeq2}}
+    end,
 
-    UserAcc2 = lists:foldl(fun({K, V}, Acc) ->
-        {?DB_CHANGES, UpdateSeq} = erlfdb_tuple:unpack(K, DbPrefix),
-        {DocId, Deleted, RevId} = erlfdb_tuple:unpack(V),
+    Start = erlfdb_tuple:pack(Start0, DbPrefix),
+    End = erlfdb_tuple:pack(End0, DbPrefix),
 
-        UpdateSeqEJson = fabric2_util:to_hex(erlfdb_tuple:pack({UpdateSeq})),
+    try
+        put('$last_changes_seq', SinceSeq0),
 
-        DelMember = if not Deleted -> []; true ->
-            [{deleted, true}]
-        end,
+        UserAcc1 = maybe_stop(UserFun(start, UserAcc0)),
 
-        {ok, NewAcc} = UserFun({change, {[
-            {seq, UpdateSeqEJson},
-            {id, DocId},
-            {changes, [{[{rev, couch_doc:rev_to_str(RevId)}]}]}
-        ] ++ DelMember}}, Acc),
+        UserAcc2 = erlfdb:fold_range(Tx, Start, End, fun({K, V}, UserAccIn) ->
+            {?DB_CHANGES, UpdateSeq} = erlfdb_tuple:unpack(K, DbPrefix),
+            {DocId, Deleted, RevId} = erlfdb_tuple:unpack(V),
 
-        NewAcc
-    end, UserAcc1, erlfdb:wait(Future)),
+            % This comes back as a versionstamp so we have
+            % to pack it to get a binary.
+            <<51:8, SeqBin:13/binary>> = erlfdb_tuple:pack({UpdateSeq}),
+            SeqHex = fabric2_util:to_hex(SeqBin),
+            put('$last_changes_seq', SeqHex),
 
-    {LastKey, _} = lists:last(erlfdb:wait(Future)),
-    {?DB_CHANGES, LastSeq} = erlfdb_tuple:unpack(LastKey, DbPrefix),
-    LastSeqEJson = fabric2_util:to_hex(erlfdb_tuple:pack({LastSeq})),
+            DelMember = if not Deleted -> []; true ->
+                [{deleted, true}]
+            end,
 
-    UserFun({stop, LastSeqEJson, 0}, UserAcc2).
+            maybe_stop(UserFun({change, {[
+                {seq, SeqHex},
+                {id, DocId},
+                {changes, [{[{rev, couch_doc:rev_to_str(RevId)}]}]}
+            ] ++ DelMember}}, UserAccIn))
+        end, UserAcc1, [{reverse, Reverse}]),
+
+        UserFun({stop, get('$last_changes_seq'), null}, UserAcc2)
+    catch throw:{stop, FinalUserAcc} ->
+        {ok, FinalUserAcc}
+    after
+        erase('$last_changes_seq')
+    end.
 
 
 get_changes(#{} = Db, Options) ->
@@ -631,11 +650,18 @@ get_changes(#{} = Db, Options) ->
     {CStart, CEnd} = erlfdb_tuple:range({?DB_CHANGES}, DbPrefix),
     Future = erlfdb:get_range(Tx, CStart, CEnd, Options),
     lists:map(fun({Key, Val}) ->
-        {?DB_CHANGES, Seq} = erlfdb_tuple:unpack(Key, DbPrefix),
-        {fabric2_util:to_hex(Seq), Val}
+        {?DB_CHANGES, SeqVS} = erlfdb_tuple:unpack(Key, DbPrefix),
+        <<51:8, SeqBin:13/binary>> = erlfdb_tuple:pack({SeqVS}),
+        {fabric2_util:to_hex(SeqBin), Val}
     end, erlfdb:wait(Future)).
 
 
+maybe_stop({ok, Acc}) ->
+    Acc;
+maybe_stop({stop, Acc}) ->
+    throw({stop, Acc}).
+
+
 debug_cluster() ->
     debug_cluster(<<>>, <<16#FE, 16#FF, 16#FF>>).
 
@@ -673,20 +699,21 @@ bump_metadata_version(Tx) ->
     erlfdb:set_versionstamped_value(Tx, ?METADATA_VERSION_KEY, <<0:112>>).
 
 
-revinfo_to_fdb(DbPrefix, DocId, #{winner := true} = RevId) ->
+revinfo_to_fdb(Tx, DbPrefix, DocId, #{winner := true} = RevId) ->
     #{
         deleted := Deleted,
         rev_id := {RevPos, Rev},
         rev_path := RevPath,
         branch_count := BranchCount
     } = RevId,
+    VS = new_versionstamp(Tx),
     Key = {?DB_REVS, DocId, not Deleted, RevPos, Rev},
-    Val = {?CURR_REV_FORMAT, ?UNSET_VS, BranchCount, list_to_tuple(RevPath)},
+    Val = {?CURR_REV_FORMAT, VS, BranchCount, list_to_tuple(RevPath)},
     KBin = erlfdb_tuple:pack(Key, DbPrefix),
     VBin = erlfdb_tuple:pack_vs(Val),
-    {KBin, VBin};
+    {KBin, VBin, VS};
 
-revinfo_to_fdb(DbPrefix, DocId, #{} = RevId) ->
+revinfo_to_fdb(_Tx, DbPrefix, DocId, #{} = RevId) ->
     #{
         deleted := Deleted,
         rev_id := {RevPos, Rev},
@@ -696,7 +723,7 @@ revinfo_to_fdb(DbPrefix, DocId, #{} = RevId) ->
     Val = {?CURR_REV_FORMAT, list_to_tuple(RevPath)},
     KBin = erlfdb_tuple:pack(Key, DbPrefix),
     VBin = erlfdb_tuple:pack(Val),
-    {KBin, VBin}.
+    {KBin, VBin, undefined}.
 
 
 fdb_to_revinfo(Key, {?CURR_REV_FORMAT, _, _, _} = Val) ->
@@ -788,6 +815,66 @@ fdb_to_local_doc(_Db, _DocId, not_found) ->
     {not_found, missing}.
 
 
+get_dir_and_bounds(DbPrefix, Options) ->
+    Reverse = case fabric2_util:get_value(dir, Options, fwd) of
+        fwd -> false;
+        rev -> true
+    end,
+    StartKey0 = fabric2_util:get_value(start_key, Options),
+    EndKeyGt = fabric2_util:get_value(end_key_gt, Options),
+    EndKey0 = fabric2_util:get_value(end_key, Options, EndKeyGt),
+    InclusiveEnd = EndKeyGt == undefined,
+
+    % CouchDB swaps the key meanings based on the direction
+    % of the fold. FoundationDB does not so we have to
+    % swap back here.
+    {StartKey1, EndKey1} = case Reverse of
+        false -> {StartKey0, EndKey0};
+        true -> {EndKey0, StartKey0}
+    end,
+
+    % Set the maximum bounds for the start and endkey
+    StartKey2 = case StartKey1 of
+        undefined -> {?DB_ALL_DOCS};
+        SK2 when is_binary(SK2) -> {?DB_ALL_DOCS, SK2}
+    end,
+
+    EndKey2 = case EndKey1 of
+        undefined -> {?DB_ALL_DOCS, <<16#FF>>};
+        EK2 when is_binary(EK2) -> {?DB_ALL_DOCS, EK2}
+    end,
+
+    StartKey3 = erlfdb_tuple:pack(StartKey2, DbPrefix),
+    EndKey3 = erlfdb_tuple:pack(EndKey2, DbPrefix),
+
+    % FoundationDB ranges are applied as SK <= key < EK
+    % By default, CouchDB is SK <= key <= EK with the
+    % optional inclusive_end=false option changing that
+    % to SK <= key < EK. Also, remember that CouchDB
+    % swaps the meaning of SK and EK based on direction.
+    %
+    % Thus we have this wonderful bit of logic to account
+    % for all of those combinations.
+
+    StartKey4 = case {Reverse, InclusiveEnd} of
+        {true, false} ->
+            erlfdb_key:first_greater_than(StartKey3);
+        _ ->
+            StartKey3
+    end,
+
+    EndKey4 = case {Reverse, InclusiveEnd} of
+        {false, true} when EndKey0 /= undefined ->
+            erlfdb_key:first_greater_than(EndKey3);
+        {true, _} ->
+            erlfdb_key:first_greater_than(EndKey3);
+        _ ->
+            EndKey3
+    end,
+
+    {Reverse, StartKey4, EndKey4}.
+
+
 get_db_handle() ->
     case get(?PDICT_DB_KEY) of
         undefined ->
@@ -843,8 +930,8 @@ execute_transaction(Tx, Fun) ->
 
 clear_transaction() ->
     fabric2_txids:remove(get(?PDICT_TX_ID_KEY)),
-    put(?PDICT_TX_ID_KEY, undefined),
-    put(?PDICT_TX_RES_KEY, undefined).
+    erase(?PDICT_TX_ID_KEY),
+    erase(?PDICT_TX_RES_KEY).
 
 
 is_commit_unknown_result() ->
@@ -868,3 +955,14 @@ get_transaction_id(Tx) ->
         TxId when is_binary(TxId) ->
             TxId
     end.
+
+
+new_versionstamp(_Tx) ->
+    % Eventually we'll have a erlfdb:get_next_tx_id(Tx)
+    % that will return a monotonically incrementing
+    % integer. For now we just hardcode 0 since we're
+    % not doing multiple docs per batch.
+    % Various utility macros
+    TxId = 0,
+    {versionstamp, 16#FFFFFFFFFFFFFFFF, 16#FFFF, TxId}.
+
diff --git a/src/fabric/src/fabric2_util.erl b/src/fabric/src/fabric2_util.erl
index def1cda..d95fd16 100644
--- a/src/fabric/src/fabric2_util.erl
+++ b/src/fabric/src/fabric2_util.erl
@@ -16,6 +16,7 @@
 -export([
     revinfo_to_path/1,
     sort_revinfos/1,
+    seq_zero/0,
 
     user_ctx_to_json/1,
 
@@ -24,6 +25,7 @@
     get_value/2,
     get_value/3,
     to_hex/1,
+    from_hex/1,
     uuid/0
 ]).
 
@@ -61,6 +63,10 @@ rev_sort_key(#{} = RevInfo) ->
     {not Deleted, RevPos, Rev}.
 
 
+seq_zero() ->
+    <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>.
+
+
 user_ctx_to_json(Db) ->
     UserCtx = fabric2_db:get_user_ctx(Db),
     {[
@@ -145,5 +151,47 @@ nibble_to_hex(I) ->
     end.
 
 
+from_hex(Bin) ->
+    iolist_to_binary(from_hex_int(Bin)).
+
+
+from_hex_int(<<>>) ->
+    [];
+from_hex_int(<<Hi:8, Lo:8, RestBinary/binary>>) ->
+    HiNib = hex_to_nibble(Hi),
+    LoNib = hex_to_nibble(Lo),
+    [<<HiNib:4, LoNib:4>> | from_hex_int(RestBinary)];
+from_hex_int(<<BadHex/binary>>) ->
+    erlang:error({invalid_hex, BadHex}).
+
+
+hex_to_nibble(N) ->
+    case N of
+        $0 -> 0;
+        $1 -> 1;
+        $2 -> 2;
+        $3 -> 3;
+        $4 -> 4;
+        $5 -> 5;
+        $6 -> 6;
+        $7 -> 7;
+        $8 -> 8;
+        $9 -> 9;
+        $a -> 10;
+        $A -> 10;
+        $b -> 11;
+        $B -> 11;
+        $c -> 12;
+        $C -> 12;
+        $d -> 13;
+        $D -> 13;
+        $e -> 14;
+        $E -> 14;
+        $f -> 15;
+        $F -> 15;
+        _ -> erlang:error({invalid_hex, N})
+    end.
+
+
 uuid() ->
     to_hex(crypto:strong_rand_bytes(16)).
diff --git a/src/fabric/test/fabric2_db_misc_tests.erl b/src/fabric/test/fabric2_db_misc_tests.erl
index e77693f..12a8f9f 100644
--- a/src/fabric/test/fabric2_db_misc_tests.erl
+++ b/src/fabric/test/fabric2_db_misc_tests.erl
@@ -61,7 +61,7 @@ empty_db_info({DbName, Db, _}) ->
 
 
 accessors({DbName, Db, _}) ->
-    SeqZero = fabric2_util:to_hex(<<0:80>>),
+    SeqZero = fabric2_util:to_hex(fabric2_util:seq_zero()),
     ?assertEqual(DbName, fabric2_db:name(Db)),
     ?assertEqual(0, fabric2_db:get_instance_start_time(Db)),
     ?assertEqual(nil, fabric2_db:get_pid(Db)),
diff --git a/src/fabric/test/fabric2_doc_count_tests.erl b/src/fabric/test/fabric2_doc_count_tests.erl
index 184cbec..37d0840 100644
--- a/src/fabric/test/fabric2_doc_count_tests.erl
+++ b/src/fabric/test/fabric2_doc_count_tests.erl
@@ -21,9 +21,9 @@
 -define(DOC_COUNT, 10).
 
 
-doc_crud_test_() ->
+doc_count_test_() ->
     {
-        "Test document CRUD operations",
+        "Test document counting operations",
         {
             setup,
             fun setup/0,
@@ -235,8 +235,6 @@ local_docs({Db, _}) ->
         ).
 
 
-
-
 get_doc_counts(Db) ->
     DocCount = fabric2_db:get_doc_count(Db),
     DelDocCount = fabric2_db:get_del_doc_count(Db),
diff --git a/src/fabric/test/fabric2_doc_fold_tests.erl b/src/fabric/test/fabric2_doc_fold_tests.erl
new file mode 100644
index 0000000..caa5f92
--- /dev/null
+++ b/src/fabric/test/fabric2_doc_fold_tests.erl
@@ -0,0 +1,209 @@
+% 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(fabric2_doc_fold_tests).
+
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(DOC_COUNT, 50).
+
+
+doc_fold_test_() ->
+    {
+        "Test document fold operations",
+        {
+            setup,
+            fun setup/0,
+            fun cleanup/1,
+            {with, [
+                fun fold_docs_basic/1,
+                fun fold_docs_rev/1,
+                fun fold_docs_with_start_key/1,
+                fun fold_docs_with_end_key/1,
+                fun fold_docs_with_both_keys_the_same/1,
+                fun fold_docs_with_different_keys/1
+            ]}
+        }
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    {ok, Db} = fabric2_db:create(?tempdb(), [{user_ctx, ?ADMIN_USER}]),
+    DocIdRevs = lists:map(fun(Val) ->
+        DocId = fabric2_util:uuid(),
+        Doc = #doc{
+            id = DocId,
+            body = {[{<<"value">>, Val}]}
+        },
+        {ok, Rev} = fabric2_db:update_doc(Db, Doc, []),
+        {DocId, couch_doc:rev_to_str(Rev)}
+    end, lists:seq(1, ?DOC_COUNT)),
+    {Db, lists:sort(DocIdRevs), Ctx}.
+
+
+cleanup({Db, _DocIdRevs, Ctx}) ->
+    ok = fabric2_db:delete(fabric2_db:name(Db), []),
+    test_util:stop_couch(Ctx).
+
+
+fold_docs_basic({Db, DocIdRevs, _}) ->
+    {ok, {?DOC_COUNT, Rows}} = fabric2_db:fold_docs(Db, fun fold_fun/2, []),
+    ?assertEqual(DocIdRevs, lists:reverse(Rows)).
+
+
+fold_docs_rev({Db, DocIdRevs, _}) ->
+    Opts = [{dir, rev}],
+    {ok, {?DOC_COUNT, Rows}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts),
+    ?assertEqual(DocIdRevs, Rows).
+
+
+fold_docs_with_start_key({Db, DocIdRevs, _}) ->
+    {StartKey, _} = hd(DocIdRevs),
+    Opts = [{start_key, StartKey}],
+    {ok, {?DOC_COUNT, Rows}}
+            = fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts),
+    ?assertEqual(DocIdRevs, lists:reverse(Rows)),
+    if length(DocIdRevs) == 1 -> ok; true ->
+        fold_docs_with_start_key({Db, tl(DocIdRevs), nil})
+    end.
+
+
+fold_docs_with_end_key({Db, DocIdRevs, _}) ->
+    RevDocIdRevs = lists:reverse(DocIdRevs),
+    {EndKey, _} = hd(RevDocIdRevs),
+    Opts = [{end_key, EndKey}],
+    {ok, {?DOC_COUNT, Rows}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts),
+    ?assertEqual(RevDocIdRevs, Rows),
+    if length(DocIdRevs) == 1 -> ok; true ->
+        fold_docs_with_end_key({Db, lists:reverse(tl(RevDocIdRevs)), nil})
+    end.
+
+
+fold_docs_with_both_keys_the_same({Db, DocIdRevs, _}) ->
+    lists:foreach(fun({DocId, _} = Row) ->
+        check_all_combos(Db, DocId, DocId, [Row])
+    end, DocIdRevs).
+
+
+fold_docs_with_different_keys({Db, DocIdRevs, _}) ->
+    lists:foreach(fun(_) ->
+        {StartKey, EndKey, Rows} = pick_range(DocIdRevs),
+        check_all_combos(Db, StartKey, EndKey, Rows)
+    end, lists:seq(1, 500)).
+
+
+check_all_combos(Db, StartKey, EndKey, Rows) ->
+    Opts1 = make_opts(fwd, StartKey, EndKey, true),
+    {ok, {?DOC_COUNT, Rows1}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts1),
+    ?assertEqual(lists:reverse(Rows), Rows1),
+
+    Opts2 = make_opts(fwd, StartKey, EndKey, false),
+    {ok, {?DOC_COUNT, Rows2}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts2),
+    Expect2 = if EndKey == undefined -> lists:reverse(Rows); true ->
+        lists:reverse(all_but_last(Rows))
+    end,
+    ?assertEqual(Expect2, Rows2),
+
+    Opts3 = make_opts(rev, StartKey, EndKey, true),
+    {ok, {?DOC_COUNT, Rows3}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts3),
+    ?assertEqual(Rows, Rows3),
+
+    Opts4 = make_opts(rev, StartKey, EndKey, false),
+    {ok, {?DOC_COUNT, Rows4}} =
+            fabric2_db:fold_docs(Db, fun fold_fun/2, [], Opts4),
+    Expect4 = if StartKey == undefined -> Rows; true ->
+        tl(Rows)
+    end,
+    ?assertEqual(Expect4, Rows4).
+
+
+
+make_opts(fwd, StartKey, EndKey, InclusiveEnd) ->
+    DirOpts = case rand:uniform() =< 0.50 of
+        true -> [{dir, fwd}];
+        false -> []
+    end,
+    StartOpts = case StartKey of
+        undefined -> [];
+        <<_/binary>> -> [{start_key, StartKey}]
+    end,
+    EndOpts = case EndKey of
+        undefined -> [];
+        <<_/binary>> when InclusiveEnd -> [{end_key, EndKey}];
+        <<_/binary>> -> [{end_key_gt, EndKey}]
+    end,
+    DirOpts ++ StartOpts ++ EndOpts;
+make_opts(rev, StartKey, EndKey, InclusiveEnd) ->
+    BaseOpts = make_opts(fwd, EndKey, StartKey, InclusiveEnd),
+    [{dir, rev}] ++ BaseOpts -- [{dir, fwd}].
+
+
+all_but_last([]) ->
+    [];
+all_but_last([_]) ->
+    [];
+all_but_last(Rows) ->
+    lists:sublist(Rows, length(Rows) - 1).
+
+
+pick_range(DocIdRevs) ->
+    {StartKey, StartRow, RestRows} = pick_start_key(DocIdRevs),
+    {EndKey, EndRow, RowsBetween} = pick_end_key(RestRows),
+    {StartKey, EndKey, StartRow ++ RowsBetween ++ EndRow}.
+
+
+pick_start_key(Rows) ->
+    case rand:uniform() =< 0.1 of
+        true ->
+            {undefined, [], Rows};
+        false ->
+            Idx = rand:uniform(length(Rows)),
+            {DocId, _} = Row = lists:nth(Idx, Rows),
+            {DocId, [Row], lists:nthtail(Idx, Rows)}
+    end.
+
+
+pick_end_key([]) ->
+    {undefined, [], []};
+
+pick_end_key(Rows) ->
+    case rand:uniform() =< 0.1 of
+        true ->
+            {undefined, [], Rows};
+        false ->
+            Idx = rand:uniform(length(Rows)),
+            {DocId, _} = Row = lists:nth(Idx, Rows),
+            Tail = lists:nthtail(Idx, Rows),
+            {DocId, [Row], Rows -- [Row | Tail]}
+    end.
+
+
+fold_fun({meta, Meta}, _Acc) ->
+    Total = fabric2_util:get_value(total, Meta),
+    {ok, {Total, []}};
+fold_fun({row, Row}, {Total, Rows}) ->
+    RowId = fabric2_util:get_value(id, Row),
+    RowId = fabric2_util:get_value(key, Row),
+    RowRev = fabric2_util:get_value(value, Row),
+    {ok, {Total, [{RowId, RowRev} | Rows]}};
+fold_fun(complete, Acc) ->
+    {ok, Acc}.