diff options
author | Paul Joseph Davis <davisp@apache.org> | 2012-01-18 20:04:54 -0600 |
---|---|---|
committer | Paul Joseph Davis <davisp@apache.org> | 2012-01-25 01:14:07 -0600 |
commit | 0ab5ebda993ee9c76ba535369538ad4fd9010257 (patch) | |
tree | d76dfb34e439a36e4552683d5e413bb7cf12f20e | |
parent | 6282b5d030ac74274fa1628a5a0b0e66293297b1 (diff) | |
download | couchdb-0ab5ebda993ee9c76ba535369538ad4fd9010257.tar.gz |
Refactor revision merging
* Refactor couch_key_tree:merge/3 and helper functions
* Rewrite couch_key_tree tests for new merge/3 functionality
* Remove merging logic from couch_db_udpater:merge_rev_trees/6
* Introduce couch_doc:merge/3 to handling revision merging
This patch does not change any outwardly visible behavior of CouchDB.
The old revision merging code was split across a couple functions with
important merging logic mixed into couch_db_udpater:merge_rev_trees/6.
This is important because couch_db_updater:merge_rev_trees/6 is also the
same code responsible for handling update_seq updates.
This patch factors out the revision merging and conflict detection into
couch_doc:merge/3 with the left over update_seq logic remaining in
couch_db_udpater:merge_rev_trees/6.
This approach is being taken so that we can reuse the revision merging
for other parts of CouchDB, notably the _local docs btree.
-rw-r--r-- | src/couchdb/couch_db_updater.erl | 104 | ||||
-rw-r--r-- | src/couchdb/couch_doc.erl | 64 | ||||
-rw-r--r-- | src/couchdb/couch_key_tree.erl | 141 | ||||
-rwxr-xr-x | test/etap/060-kt-merging.t | 183 |
4 files changed, 264 insertions, 228 deletions
diff --git a/src/couchdb/couch_db_updater.erl b/src/couchdb/couch_db_updater.erl index 54531db39..0bfe951a2 100644 --- a/src/couchdb/couch_db_updater.erl +++ b/src/couchdb/couch_db_updater.erl @@ -572,82 +572,37 @@ send_result(Client, Ref, NewResult) -> % used to send a result to the client catch(Client ! {result, self(), {Ref, NewResult}}). -merge_rev_trees(_Limit, _Merge, [], [], AccNewInfos, AccRemoveSeqs, AccSeq) -> +merge_rev_trees([], [], AccNewInfos, AccRemoveSeqs, AccSeq, _Opts) -> {ok, lists:reverse(AccNewInfos), AccRemoveSeqs, AccSeq}; -merge_rev_trees(Limit, MergeConflicts, [NewDocs|RestDocsList], - [OldDocInfo|RestOldInfo], AccNewInfos, AccRemoveSeqs, AccSeq) -> - #full_doc_info{id=Id,rev_tree=OldTree,deleted=OldDeleted0,update_seq=OldSeq} - = OldDocInfo, - {NewRevTree, _} = lists:foldl( - fun({Client, {#doc{revs={Pos,[_Rev|PrevRevs]}}=NewDoc, Ref}}, {AccTree, OldDeleted}) -> - if not MergeConflicts -> - case couch_key_tree:merge(AccTree, couch_doc:to_path(NewDoc), - Limit) of - {_NewTree, conflicts} when (not OldDeleted) -> - send_result(Client, Ref, conflict), - {AccTree, OldDeleted}; - {NewTree, conflicts} when PrevRevs /= [] -> - % Check to be sure if prev revision was specified, it's - % a leaf node in the tree - Leafs = couch_key_tree:get_all_leafs(AccTree), - IsPrevLeaf = lists:any(fun({_, {LeafPos, [LeafRevId|_]}}) -> - {LeafPos, LeafRevId} == {Pos-1, hd(PrevRevs)} - end, Leafs), - if IsPrevLeaf -> - {NewTree, OldDeleted}; - true -> - send_result(Client, Ref, conflict), - {AccTree, OldDeleted} - end; - {NewTree, no_conflicts} when AccTree == NewTree -> - % the tree didn't change at all - % meaning we are saving a rev that's already - % been editted again. - if (Pos == 1) and OldDeleted -> - % this means we are recreating a brand new document - % into a state that already existed before. - % put the rev into a subsequent edit of the deletion - #doc_info{revs=[#rev_info{rev={OldPos,OldRev}}|_]} = - couch_doc:to_doc_info(OldDocInfo), - NewRevId = couch_db:new_revid( - NewDoc#doc{revs={OldPos, [OldRev]}}), - NewDoc2 = NewDoc#doc{revs={OldPos + 1, [NewRevId, OldRev]}}, - {NewTree2, _} = couch_key_tree:merge(AccTree, - couch_doc:to_path(NewDoc2), Limit), - % we changed the rev id, this tells the caller we did - send_result(Client, Ref, {ok, {OldPos + 1, NewRevId}}), - {NewTree2, OldDeleted}; - true -> - send_result(Client, Ref, conflict), - {AccTree, OldDeleted} - end; - {NewTree, _} -> - {NewTree, NewDoc#doc.deleted} - end; - true -> - {NewTree, _} = couch_key_tree:merge(AccTree, - couch_doc:to_path(NewDoc), Limit), - {NewTree, OldDeleted} - end - end, - {OldTree, OldDeleted0}, NewDocs), - if NewRevTree == OldTree -> - % nothing changed - merge_rev_trees(Limit, MergeConflicts, RestDocsList, RestOldInfo, - AccNewInfos, AccRemoveSeqs, AccSeq); - true -> - % we have updated the document, give it a new seq # - NewInfo = #full_doc_info{id=Id,update_seq=AccSeq+1,rev_tree=NewRevTree}, - RemoveSeqs = case OldSeq of - 0 -> AccRemoveSeqs; - _ -> [OldSeq | AccRemoveSeqs] - end, - merge_rev_trees(Limit, MergeConflicts, RestDocsList, RestOldInfo, - [NewInfo|AccNewInfos], RemoveSeqs, AccSeq+1) +merge_rev_trees([NewDocs|RestDocsList], [OldDocInfo|RestOldInfo], + AccNewInfos, AccRemoveSeqs, AccSeq, Opts) -> + NewDocInfo = lists:foldl(fun({Client, {NewDoc, Ref}}, AccDocInfo) -> + case couch_doc:merge(AccDocInfo, NewDoc, Opts) of + {ok, NewInfo} -> + NewInfo; + {ok, NewInfo, NewRev} -> + send_result(Client, Ref, {ok, NewRev}), + NewInfo; + conflict -> + send_result(Client, Ref, conflict), + AccDocInfo + end + end, OldDocInfo, NewDocs), + case NewDocInfo#full_doc_info.update_seq of + increment -> + RemSeqs = case OldDocInfo#full_doc_info.update_seq of + 0 -> AccRemoveSeqs; + N -> [N | AccRemoveSeqs] + end, + NewDocInfo1 = NewDocInfo#full_doc_info{update_seq=AccSeq + 1}, + merge_rev_trees(RestDocsList, RestOldInfo, + [NewDocInfo1 | AccNewInfos], RemSeqs, AccSeq + 1, Opts); + _ -> + merge_rev_trees(RestDocsList, RestOldInfo, + [NewDocInfo | AccNewInfos], AccRemoveSeqs, AccSeq, Opts) end. - new_index_entries([], AccById, AccBySeq, AccDDocIds) -> {AccById, AccBySeq, AccDDocIds}; new_index_entries([FullDocInfo|RestInfos], AccById, AccBySeq, AccDDocIds) -> @@ -687,8 +642,9 @@ update_docs_int(Db, DocsList, NonRepDocs, MergeConflicts, FullCommit) -> end, Ids, OldDocLookups), % Merge the new docs into the revision trees. - {ok, NewFullDocInfos, RemoveSeqs, NewSeq} = merge_rev_trees(RevsLimit, - MergeConflicts, DocsList, OldDocInfos, [], [], LastSeq), + Options = [{merge_conflicts, MergeConflicts}, {revs_limit, RevsLimit}], + {ok, NewFullDocInfos, RemoveSeqs, NewSeq} = merge_rev_trees(DocsList, + OldDocInfos, [], [], LastSeq, Options), % All documents are now ready to write. diff --git a/src/couchdb/couch_doc.erl b/src/couchdb/couch_doc.erl index 7b7233ed8..801b1d3d5 100644 --- a/src/couchdb/couch_doc.erl +++ b/src/couchdb/couch_doc.erl @@ -13,6 +13,7 @@ -module(couch_doc). -export([to_doc_info/1,to_doc_info_path/1,parse_rev/1,parse_revs/1,rev_to_str/1,revs_to_strs/1]). +-export([merge/3]). -export([att_foldl/3,range_att_foldl/5,att_foldl_decode/3,get_validate_doc_fun/1]). -export([from_json_obj/1,to_json_obj/2,has_stubs/1, merge_stubs/2]). -export([validate_docid/1]). @@ -25,6 +26,69 @@ -include("couch_db.hrl"). + +merge(#full_doc_info{}=OldDoc, #doc{revs={NewPos, _}}=NewDoc, Options) -> + #full_doc_info{ + rev_tree=OldTree, + deleted=OldDeleted + } = OldDoc, + #doc{ + revs={NewPos, _} + } = NewDoc, + RevsLimit = couch_util:get_value(revs_limit, Options, 1000), + MergeConflicts = couch_util:get_value(merge_conflicts, Options, false), + case couch_key_tree:merge(OldTree, to_path(NewDoc), RevsLimit) of + {NewTree, new_leaf} -> + % Happy case created a new leaf + {ok, OldDoc#full_doc_info{ + rev_tree=NewTree, + deleted=NewDoc#doc.deleted, + update_seq=increment + }}; + {_, internal_node} when OldDeleted, not MergeConflicts -> + % Recreating a deleted document into a state that + % previously existed. Create a new revision and + % carry on. + #doc_info{ + revs=[#rev_info{rev={OldPos, OldRev}} | _] + } = couch_doc:to_doc_info(OldDoc), + NewRevId = couch_db:new_revid(NewDoc#doc{revs={OldPos, [OldRev]}}), + NewDoc1 = NewDoc#doc{revs={OldPos + 1, [NewRevId, OldRev]}}, + {Tree, _} = couch_key_tree:merge(OldTree, to_path(NewDoc1), RevsLimit), + {ok, OldDoc#full_doc_info{ + rev_tree=Tree, + deleted=NewDoc#doc.deleted, + update_seq=increment + }, {OldPos + 1, NewRevId}}; + {NewTree, new_branch} when OldDeleted, not MergeConflicts, NewPos == 1 -> + % Recreating a deleted document into a new state. + {ok, OldDoc#full_doc_info{ + rev_tree=NewTree, + deleted=NewDoc#doc.deleted, + update_seq=increment + }}; + {NewTree, internal_node} when MergeConflicts -> + % Replication gave us something we already had so + % just update the rev tree and carry on. We keep + % the new revision tree in case we're recovering + % from COUCHDB-968 and friends but deletion and + % update_seqs should not be affected. + {ok, OldDoc#full_doc_info{ + rev_tree=NewTree + }}; + {NewTree, _} when MergeConflicts -> + % Replication gave us a new leaf or branch + % so we need to bump the update_seq and + % deletion status. + {ok, OldDoc#full_doc_info{ + rev_tree=NewTree, + deleted=OldDeleted and NewDoc#doc.deleted, + update_seq=increment + }}; + _ -> + conflict + end. + -spec to_path(#doc{}) -> path(). to_path(#doc{revs={Start, RevIds}}=Doc) -> [Branch] = to_branch(Doc, lists:reverse(RevIds)), diff --git a/src/couchdb/couch_key_tree.erl b/src/couchdb/couch_key_tree.erl index ce45ab81b..20683e6b9 100644 --- a/src/couchdb/couch_key_tree.erl +++ b/src/couchdb/couch_key_tree.erl @@ -54,8 +54,6 @@ -include("couch_db.hrl"). %% @doc Merge a path with a list of paths and stem to the given length. --spec merge([path()], path(), pos_integer()) -> {[path()], - conflicts | no_conflicts}. merge(Paths, Path, Depth) -> {Merged, Conflicts} = merge(Paths, Path), {stem(Merged, Depth), Conflicts}. @@ -63,93 +61,80 @@ merge(Paths, Path, Depth) -> %% @doc Merge a path with an existing list of paths, returning a new list of %% paths. A return of conflicts indicates a new conflict was discovered in this %% merge. Conflicts may already exist in the original list of paths. --spec merge([path()], path()) -> {[path()], conflicts | no_conflicts}. merge(Paths, Path) -> - {ok, Merged, HasConflicts} = merge_one(Paths, Path, [], false), - if HasConflicts -> - Conflicts = conflicts; - (length(Merged) =/= length(Paths)) and (length(Merged) =/= 1) -> - Conflicts = conflicts; - true -> - Conflicts = no_conflicts - end, - {lists:sort(Merged), Conflicts}. - --spec merge_one(Original::[path()], Inserted::path(), [path()], boolean()) -> - {ok, Merged::[path()], NewConflicts::boolean()}. -merge_one([], Insert, OutAcc, ConflictsAcc) -> - {ok, [Insert | OutAcc], ConflictsAcc}; -merge_one([{Start, Tree}|Rest], {StartInsert, TreeInsert}, Acc, HasConflicts) -> - case merge_at([Tree], StartInsert - Start, [TreeInsert]) of - {ok, [Merged], Conflicts} -> - MergedStart = lists:min([Start, StartInsert]), - {ok, Rest ++ [{MergedStart, Merged} | Acc], Conflicts or HasConflicts}; - no -> - AccOut = [{Start, Tree} | Acc], - merge_one(Rest, {StartInsert, TreeInsert}, AccOut, HasConflicts) + {Merged, Status} = merge_one(Paths, Path, []), + {lists:sort(Merged), Status}. + +merge_one([], Path, []) -> + % Special case when the doc is created and has + % no revisions to merge with + {[Path], new_leaf}; +merge_one([], Path, Acc) -> + % Merge created a new top-level branch + {[Path | Acc], new_branch}; +merge_one([{Start, Tree} | Rest], {PathStart, PathVals}, Acc) -> + try + {[Merged], Status} = merge_at([Tree], PathStart - Start, [PathVals]), + MergedStart = lists:min([Start, PathStart]), + {Rest ++ [{MergedStart, Merged} | Acc], Status} + catch throw:no_merge -> + merge_one(Rest, {PathStart, PathVals}, [{Start, Tree} | Acc]) end. --spec merge_at(tree(), Place::integer(), tree()) -> - {ok, Merged::tree(), HasConflicts::boolean()} | no. -merge_at(_Ours, _Place, []) -> - no; -merge_at([], _Place, _Insert) -> - no; -merge_at([{Key, Value, SubTree}|Sibs], Place, InsertTree) when Place > 0 -> - % inserted starts later than committed, need to drill into committed subtree - case merge_at(SubTree, Place - 1, InsertTree) of - {ok, Merged, Conflicts} -> - {ok, [{Key, Value, Merged} | Sibs], Conflicts}; - no -> +merge_at([], _Depth, _Path) -> + throw(no_merge); +merge_at([{Key, Value, SubTree} | Sibs], Depth, Path) when Depth > 0 -> + % inserted starts later than committed, + % need to drill into committed subtree + try + {Merged0, Status0} = merge_at(SubTree, Depth - 1, Path), + {[{Key, Value, Merged0} | Sibs], Status0} + catch throw:no_merge -> % first branch didn't merge, move to next branch - case merge_at(Sibs, Place, InsertTree) of - {ok, Merged, Conflicts} -> - {ok, [{Key, Value, SubTree} | Merged], Conflicts}; - no -> - no - end - end; -merge_at(OurTree, Place, [{Key, Value, SubTree}]) when Place < 0 -> - % inserted starts earlier than committed, need to drill into insert subtree - case merge_at(OurTree, Place + 1, SubTree) of - {ok, Merged, Conflicts} -> - {ok, [{Key, Value, Merged}], Conflicts}; - no -> - no + {Merged1, Status1} = merge_at(Sibs, Depth, Path), + {[{Key, Value, SubTree} | Merged1], Status1} end; -merge_at([{Key, V1, SubTree}|Sibs], 0, [{Key, V2, InsertSubTree}]) -> - {Merged, Conflicts} = merge_simple(SubTree, InsertSubTree), - {ok, [{Key, value_pref(V1, V2), Merged} | Sibs], Conflicts}; +merge_at(OurTree, Depth, [{Key, Value, SubPath}]) when Depth < 0 -> + % inserted starts earlier than committed, + % need to drill into insert subtree + {Merged, Status} = merge_at(OurTree, Depth + 1, SubPath), + {[{Key, Value, Merged}], Status}; +merge_at([{Key, V1, SubTree} | Sibs], 0, [{Key, V2, SubPath}]) -> + {Merged, Status} = merge_simple(SubTree, SubPath), + {[{Key, value_pref(V1, V2), Merged} | Sibs], Status}; merge_at([{OurKey, _, _} | _], 0, [{Key, _, _}]) when OurKey > Key -> % siblings keys are ordered, no point in continuing - no; + throw(no_merge); merge_at([Tree | Sibs], 0, InsertTree) -> - case merge_at(Sibs, 0, InsertTree) of - {ok, Merged, Conflicts} -> - {ok, [Tree | Merged], Conflicts}; - no -> - no - end. - -% key tree functions + {Merged, Status} = merge_at(Sibs, 0, InsertTree), + {[Tree | Merged], Status}. --spec merge_simple(tree(), tree()) -> {Merged::tree(), NewConflicts::boolean()}. +merge_simple([], []) -> + % Inserted path lands on a leaf + {[], internal_node}; merge_simple([], B) -> - {B, false}; + % Inserted path extends a leaf + {B, new_leaf}; merge_simple(A, []) -> - {A, false}; -merge_simple([{Key, V1, SubA} | NextA], [{Key, V2, SubB} | NextB]) -> - {MergedSubTree, Conflict1} = merge_simple(SubA, SubB), - {MergedNextTree, Conflict2} = merge_simple(NextA, NextB), - Value = value_pref(V1, V2), - {[{Key, Value, MergedSubTree} | MergedNextTree], Conflict1 or Conflict2}; -merge_simple([{A, _, _} = Tree | Next], [{B, _, _} | _] = Insert) when A < B -> - {Merged, Conflict} = merge_simple(Next, Insert), - % if Merged has more branches than the input we added a new conflict - {[Tree | Merged], Conflict orelse (length(Merged) > length(Next))}; -merge_simple(Ours, [Tree | Next]) -> - {Merged, Conflict} = merge_simple(Ours, Next), - {[Tree | Merged], Conflict orelse (length(Merged) > length(Next))}. + % Ran out of insertion path at an internal node + {A, internal_node}; +merge_simple([{Key, V1, SubTree} | NextTree], [{Key, V2, SubPath}]) -> + % Keys match, continue descending along this branch + {Merged, Status} = merge_simple(SubTree, SubPath), + {[{Key, value_pref(V1, V2), Merged} | NextTree], Status}; +merge_simple([{A, _, _} = Tree | Siblings], [{B, _, _}] = Path) when A < B -> + % Keep trying siblings until we run out or find a + % key A > B + {Merged, Status0} = merge_simple(Siblings, Path), + Status = case length(Merged) == length(Siblings) of + true -> Status0; + false -> new_branch + end, + {[Tree | Merged], Status}; +merge_simple(Tree, [Path]) -> + % Sorted keys means we know the rest of the path + % is a new branch + {[Path | Tree], new_branch}. find_missing(_Tree, []) -> []; diff --git a/test/etap/060-kt-merging.t b/test/etap/060-kt-merging.t index efbdbf695..f44e40d9d 100755 --- a/test/etap/060-kt-merging.t +++ b/test/etap/060-kt-merging.t @@ -26,123 +26,154 @@ main(_) -> ok. test() -> - One = {1, {"1","foo",[]}}, - etap:is( - {[One], no_conflicts}, - couch_key_tree:merge([], One, 10), - "The empty tree is the identity for merge." + Simple = {1, {"1","foo",[]}}, + TwoDeep0 = {1, {"1", "foo", [{"2_0", "bar", []}]}}, + TwoDeep1 = {1, {"1", "foo", [{"2_1", "baz", []}]}}, + NewLeaf = {2, {"2_0", "bar", [{"3", "bing", []}]}}, + WithNewLeaf = {1, {"1", "foo", [{"2_0", "bar", [{"3", "bing", []}]}]}}, + NewBranch = {1, {"1", "foo", [{"2_0", "bar", []}, {"2_1", "baz", []}]}}, + NewDeepBranch = {2, {"2_0", "bar", [{"3_1", "bang", []}]}}, + + StemmedEdit = {3, {"3", "bing", []}}, + StemmedConflicts = [Simple, StemmedEdit], + + NewBranchLeaf = {1, + {"1", "foo", [ + {"2_0", "bar", [ + {"3", "bing", []} + ]}, + {"2_1", "baz", []} + ]} + }, + + NewBranchLeafBranch = {1, + {"1", "foo", [ + {"2_0", "bar", [ + {"3", "bing", []}, + {"3_1", "bang", []} + ]}, + {"2_1", "baz", []} + ]} + }, + + Stemmed2 = [ + {1, {"1", "foo", [ + {"2_1", "baz", []} + ]}}, + {2, {"2_0", "bar", [ + {"3", "bing", []}, + {"3_1", "bang", []} + ]}} + ], + + Stemmed3 = [ + {2, {"2_1", "baz", []}}, + {3, {"3", "bing", []}}, + {3, {"3_1", "bang", []}} + ], + + PartialRecover = [ + {1, {"1", "foo", [ + {"2_0", "bar", [ + {"3", "bing", []} + ]} + ]}}, + {2, {"2_1", "baz", []}}, + {3, {"3_1", "bang", []}} + ], + + etap:is( + couch_key_tree:merge([], Simple, 10), + {[Simple], new_leaf}, + "Merging a path into an empty tree is the path" ), + etap:is( - {[One], no_conflicts}, - couch_key_tree:merge([One], One, 10), - "Merging is reflexive." + couch_key_tree:merge([Simple], Simple, 10), + {[Simple], internal_node}, + "Remerge path into path is reflexive" ), - TwoSibs = [{1, {"1","foo",[]}}, - {1, {"2","foo",[]}}], - etap:is( - {TwoSibs, no_conflicts}, - couch_key_tree:merge(TwoSibs, One, 10), - "Merging a prefix of a tree with the tree yields the tree." + couch_key_tree:merge([], TwoDeep0, 10), + {[TwoDeep0], new_leaf}, + "Merging a path with multiple entries is the path" ), - Three = {1, {"3","foo",[]}}, - ThreeSibs = [{1, {"1","foo",[]}}, - {1, {"2","foo",[]}}, - {1, {"3","foo",[]}}], - etap:is( - {ThreeSibs, conflicts}, - couch_key_tree:merge(TwoSibs, Three, 10), - "Merging a third unrelated branch leads to a conflict." + couch_key_tree:merge([TwoDeep0], TwoDeep0, 10), + {[TwoDeep0], internal_node}, + "Merging a path with multiple entries is reflexive" ), - - TwoChild = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}, - etap:is( - {[TwoChild], no_conflicts}, - couch_key_tree:merge([TwoChild], TwoChild, 10), - "Merging two children is still reflexive." + couch_key_tree:merge([TwoDeep0], Simple, 10), + {[TwoDeep0], internal_node}, + "Merging a subpath into a path results in the path" ), - TwoChildSibs = {1, {"1","foo", [{"1a", "bar", []}, - {"1b", "bar", []}]}}, etap:is( - {[TwoChildSibs], no_conflicts}, - couch_key_tree:merge([TwoChildSibs], TwoChildSibs, 10), - "Merging a tree to itself is itself."), - - TwoChildPlusSibs = - {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}, - {"1b", "bar", []}]}}, + couch_key_tree:merge([TwoDeep0], NewLeaf, 10), + {[WithNewLeaf], new_leaf}, + "Merging a new leaf gives us a new leaf" + ), etap:is( - {[TwoChildPlusSibs], no_conflicts}, - couch_key_tree:merge([TwoChild], TwoChildSibs, 10), - "Merging tree of uneven length at node 2."), + couch_key_tree:merge([TwoDeep0], TwoDeep1, 10), + {[NewBranch], new_branch}, + "Merging a new branch returns a proper tree" + ), - Stemmed1b = {2, {"1a", "bar", []}}, etap:is( - {[TwoChildSibs], no_conflicts}, - couch_key_tree:merge([TwoChildSibs], Stemmed1b, 10), - "Merging a tree with a stem." + couch_key_tree:merge([TwoDeep1], TwoDeep0, 10), + {[NewBranch], new_branch}, + "Order of merging does not affect the resulting tree" ), - TwoChildSibs2 = {1, {"1","foo", [{"1a", "bar", []}, - {"1b", "bar", [{"1bb", "boo", []}]}]}}, - Stemmed1bb = {3, {"1bb", "boo", []}}, etap:is( - {[TwoChildSibs2], no_conflicts}, - couch_key_tree:merge([TwoChildSibs2], Stemmed1bb, 10), - "Merging a stem at a deeper level." + couch_key_tree:merge([NewBranch], NewLeaf, 10), + {[NewBranchLeaf], new_leaf}, + "Merging a new_leaf doesn't return new_branch when branches exist" ), - StemmedTwoChildSibs2 = [{2,{"1a", "bar", []}}, - {2,{"1b", "bar", [{"1bb", "boo", []}]}}], - etap:is( - {StemmedTwoChildSibs2, no_conflicts}, - couch_key_tree:merge(StemmedTwoChildSibs2, Stemmed1bb, 10), - "Merging a stem at a deeper level against paths at deeper levels." + couch_key_tree:merge([NewBranchLeaf], NewDeepBranch, 10), + {[NewBranchLeafBranch], new_branch}, + "Merging a deep branch with branches works" ), - Stemmed1aa = {3, {"1aa", "bar", []}}, etap:is( - {[TwoChild], no_conflicts}, - couch_key_tree:merge([TwoChild], Stemmed1aa, 10), - "Merging a single tree with a deeper stem." + couch_key_tree:merge(StemmedConflicts, WithNewLeaf, 10), + {[WithNewLeaf], new_leaf}, + "New information reconnects steming induced conflicts" ), - Stemmed1a = {2, {"1a", "bar", [{"1aa", "bar", []}]}}, etap:is( - {[TwoChild], no_conflicts}, - couch_key_tree:merge([TwoChild], Stemmed1a, 10), - "Merging a larger stem." + couch_key_tree:merge([TwoDeep0], NewLeaf, 2), + {[NewLeaf], new_leaf}, + "Simple stemming works" ), etap:is( - {[Stemmed1a], no_conflicts}, - couch_key_tree:merge([Stemmed1a], Stemmed1aa, 10), - "More merging." + couch_key_tree:merge([NewBranchLeafBranch], Simple, 2), + {Stemmed2, internal_node}, + "Merge with stemming works correctly for branches" ), - OneChild = {1, {"1","foo",[{"1a", "bar", []}]}}, - Expect1 = [OneChild, Stemmed1aa], etap:is( - {Expect1, conflicts}, - couch_key_tree:merge([OneChild], Stemmed1aa, 10), - "Merging should create conflicts." + couch_key_tree:merge([NewBranchLeafBranch], Simple, 1), + {Stemmed3, internal_node}, + "Merge with stemming to leaves works fine" ), etap:is( - {[TwoChild], no_conflicts}, - couch_key_tree:merge(Expect1, TwoChild, 10), - "Merge should have no conflicts." + couch_key_tree:merge(Stemmed3, WithNewLeaf, 10), + {PartialRecover, internal_node}, + "Merging unstemmed recovers as much as possible without losing info" ), + %% this test is based on couch-902-test-case2.py %% foo has conflicts from replication at depth two %% foo3 is the current value @@ -168,7 +199,7 @@ test() -> ]}}, etap:is( - {[FooBar], no_conflicts}, + {[FooBar], new_leaf}, couch_key_tree:merge([Foo],Bar,10), "Merging trees with conflicts ought to behave." ), |