diff options
author | Paul J. Davis <paul.joseph.davis@gmail.com> | 2019-05-01 13:19:10 -0500 |
---|---|---|
committer | Paul J. Davis <paul.joseph.davis@gmail.com> | 2019-05-01 13:19:10 -0500 |
commit | 104d19d4a83ab978af4c7537abe57f9b41812445 (patch) | |
tree | 22a2c53d1a007678db11f7490ed986ed977d57b6 | |
parent | b1d31e2af49f6b1fadeb3ea55f188886f4e26b70 (diff) | |
download | couchdb-104d19d4a83ab978af4c7537abe57f9b41812445.tar.gz |
Moar tests
-rw-r--r-- | src/fabric/src/fabric2.hrl | 5 | ||||
-rw-r--r-- | src/fabric/src/fabric2_db.erl | 2 | ||||
-rw-r--r-- | src/fabric/src/fabric2_fdb.erl | 226 | ||||
-rw-r--r-- | src/fabric/src/fabric2_util.erl | 48 | ||||
-rw-r--r-- | src/fabric/test/fabric2_db_misc_tests.erl | 2 | ||||
-rw-r--r-- | src/fabric/test/fabric2_doc_count_tests.erl | 6 | ||||
-rw-r--r-- | 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 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}. |