summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul J. Davis <paul.joseph.davis@gmail.com>2019-05-01 13:19:10 -0500
committerPaul J. Davis <paul.joseph.davis@gmail.com>2019-05-01 13:19:10 -0500
commit104d19d4a83ab978af4c7537abe57f9b41812445 (patch)
tree22a2c53d1a007678db11f7490ed986ed977d57b6
parentb1d31e2af49f6b1fadeb3ea55f188886f4e26b70 (diff)
downloadcouchdb-104d19d4a83ab978af4c7537abe57f9b41812445.tar.gz
Moar tests
-rw-r--r--src/fabric/src/fabric2.hrl5
-rw-r--r--src/fabric/src/fabric2_db.erl2
-rw-r--r--src/fabric/src/fabric2_fdb.erl226
-rw-r--r--src/fabric/src/fabric2_util.erl48
-rw-r--r--src/fabric/test/fabric2_db_misc_tests.erl2
-rw-r--r--src/fabric/test/fabric2_doc_count_tests.erl6
-rw-r--r--src/fabric/test/fabric2_doc_fold_tests.erl209
7 files changed, 423 insertions, 75 deletions
diff --git a/src/fabric/src/fabric2.hrl b/src/fabric/src/fabric2.hrl
index 451c77954..e8d0b13c9 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 680372e5e..9971bbc8f 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 b0f85feb2..19b19bb4b 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 def1cdac8..d95fd160f 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 e77693f59..12a8f9f21 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 184cbec32..37d08404d 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 000000000..caa5f925a
--- /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}.