couch_db:update_doc/4

前回からの続きで、couch_db:update_docs/4の続きを。

couch_db.erl:

 1    case (Db#db.validate_doc_funs /= []) orelse
 2        lists:any(
 3            fun({#doc{id= <<?DESIGN_DOC_PREFIX, _/binary>>}, _Ref}) ->
 4                true;
 5            ({#doc{atts=Atts}, _Ref}) ->
 6                Atts /= []
 7            end, Docs3) of
 8    true ->
 9        % lookup the doc by id and get the most recent
10        Ids = [Id || [{#doc{id=Id}, _Ref}|_] <- DocBuckets],
11        ExistingDocInfos = get_full_doc_infos(Db, Ids),
12
13        {DocBucketsPrepped, PreCommitFailures} = prep_and_validate_updates(Db,
14                DocBuckets, ExistingDocInfos, AllOrNothing, [], []),
15
16        % strip out any empty buckets
17        DocBuckets2 = [Bucket || [_|_] = Bucket <- DocBucketsPrepped];
18    false ->
19        PreCommitFailures = [],
20        DocBuckets2 = DocBuckets
21    end,
22```

#db.validate_doc_funsに何か関数がバインドされているか、更新対象のドキュメントの中にDesignDocが含まれている場合はprep_and_validate_updates/6を呼び出します。

couch_db:prep_and_validate_updates/6

PreCommitFiluresはvalidationに引っかかった結果が返るのだと思いますが、DocBucketsPreppedが分からないので、この関数を追ってみます。

couch_db.erl:

 1{% raw %}
 2prep_and_validate_updates(_Db, [], [], _AllowConflict, AccPrepped,
 3       AccFatalErrors) ->
 4   {AccPrepped, AccFatalErrors};
 5prep_and_validate_updates(Db, [DocBucket|RestBuckets], [not_found|RestLookups],
 6        AllowConflict, AccPrepped, AccErrors) ->
 7    {PreppedBucket, AccErrors3} = lists:foldl(
 8        fun({#doc{revs=Revs}=Doc,Ref}, {AccBucket, AccErrors2}) ->
 9            case couch_doc:has_stubs(Doc) of
10            true ->
11                couch_doc:merge_stubs(Doc, #doc{}); % will throw exception
12            false -> ok
13            end,
14            case Revs of
15            {0, []} ->
16                case validate_doc_update(Db, Doc, fun() -> nil end) of
17                ok ->
18                    {[{Doc, Ref} | AccBucket], AccErrors2};
19                Error ->
20                    {AccBucket, [{Ref, Error} | AccErrors2]}
21                end;
22            _ ->
23                % old revs specified but none exist, a conflict
24                {AccBucket, [{Ref, conflict} | AccErrors2]}
25            end
26        end,
27        {[], AccErrors}, DocBucket),
28
29    prep_and_validate_updates(Db, RestBuckets, RestLookups, AllowConflict,
30            [lists:reverse(PreppedBucket) | AccPrepped], AccErrors3);
31prep_and_validate_updates(Db, [DocBucket|RestBuckets],
32        [{ok, #full_doc_info{rev_tree=OldRevTree}=OldFullDocInfo}|RestLookups],
33        AllowConflict, AccPrepped, AccErrors) ->
34    Leafs = couch_key_tree:get_all_leafs(OldRevTree),
35    LeafRevsDict = dict:from_list([
36        begin
37            Deleted = element(1, LeafVal),
38            Sp = element(2, LeafVal),
39
40            {{Start, RevId}, {Deleted, Sp, Revs}}
41        end ||
42        {LeafVal, {Start, [RevId | _]} = Revs} <- Leafs
43    ]),
44    {PreppedBucket, AccErrors3} = lists:foldl(
45        fun({Doc, Ref}, {Docs2Acc, AccErrors2}) ->
46            case prep_and_validate_update(Db, Doc, OldFullDocInfo,
47                    LeafRevsDict, AllowConflict) of
48            {ok, Doc2} ->
49                {[{Doc2, Ref} | Docs2Acc], AccErrors2};
50            {Error, #doc{}} ->
51                % Record the error
52                {Docs2Acc, [{Ref, Error} |AccErrors2]}
53            end
54        end,
55        {[], AccErrors}, DocBucket),
56    prep_and_validate_updates(Db, RestBuckets, RestLookups, AllowConflict,
57            [PreppedBucket | AccPrepped], AccErrors3).
58{% endraw %}

最初のパターンマッチは、更新対象ドキュメントが無い(無くなった)ものなので読み飛ばします。次のExistingDocInfosのheadがnot_foundのパターンマッチを見ていきます。

couch_doc:has_stubs/1を呼び出し、その結果がtrueであればcouch_doc:merge_stubs/2を呼び出して空のドキュメント(#doc{})とマージしているように見えます。stubとは何かを見たいので、couch_doc:has_stubs/1のコードを見てみます。

couch_doc.erl:

1has_stubs(#doc{atts=Atts}) ->
2    has_stubs(Atts);
3has_stubs([]) ->
4    false;
5has_stubs([#att{data=stub}|_]) ->
6    true;
7has_stubs([_Att|Rest]) ->
8    has_stubs(Rest).

ドキュメントのアタッチメントのdatastubのものが1つでもあればtrueが返るようになっています。そのままcouch_doc:merge_stubs/2を見てみます。

couch_db.erl:

 1merge_stubs(#doc{id = Id}, nil) ->
 2    throw({missing_stub, <<"Previous revision missing for document ", Id/binary>>});
 3merge_stubs(#doc{id=Id,atts=MemBins}=StubsDoc, #doc{atts=DiskBins}) ->
 4    BinDict = dict:from_list([{Name, Att} || #att{name=Name}=Att <- DiskBins]),
 5    MergedBins = lists:map(
 6        fun(#att{name=Name, data=stub, revpos=StubRevPos}) ->
 7            case dict:find(Name, BinDict) of
 8            {ok, #att{revpos=DiskRevPos}=DiskAtt}
 9                    when DiskRevPos == StubRevPos orelse StubRevPos == nil ->
10                DiskAtt;
11            _ ->
12                throw({missing_stub,
13                        <<"id:", Id/binary, ", name:", Name/binary>>})
14            end;
15        (Att) ->
16            Att
17        end, MemBins),
18    StubsDoc#doc{atts= MergedBins}.

第二引数のドキュメントのアタッチメント(DiskBins)の名前をキーにしてdictionaryを作成し、第一引数のドキュメントのアタッチメント(MemBins)の名前と被っているのがあれば、DiskBins側のアタッチメントを採用する、というコードのようです。ただ、prep_and_validate_updates/6から呼び出す場合、第二引数は#doc{}なので、DiskBinsundefinedになるんじゃないかな…。呼び出し側のコメントにも

% will throw exception

とあるし、動くことを想定しているコードなのかよく分かりませんでした。prep_and_validate_updates/6に戻り、バリデーションの結果問題がなければPreppedBucketの方に、問題があればAccErrorsの方にドキュメントが振り分けられます。ここまでが、ドキュメントが見つからずnot_foundだった場合の話。

prep_and_validate_updates/6で最後にパターンマッチする関数を見ていきます。couch_key_tree:get_all_leafs/1で更新前のrev_treeのleafを取得しています。

couch_key_tree.erl:

 1get_all_leafs(Trees) ->
 2    get_all_leafs(Trees, []).
 3
 4get_all_leafs([], Acc) ->
 5    Acc;
 6get_all_leafs([{Pos, Tree}|Rest], Acc) ->
 7    get_all_leafs(Rest, get_all_leafs_simple(Pos, [Tree], []) ++ Acc).
 8
 9get_all_leafs_simple(_Pos, [], _KeyPathAcc) ->
10    [];
11get_all_leafs_simple(Pos, [{KeyId, Value, []} | RestTree], KeyPathAcc) ->
12    [{Value, {Pos, [KeyId | KeyPathAcc]}} | get_all_leafs_simple(Pos, RestTree, KeyPathAcc)];
13get_all_leafs_simple(Pos, [{KeyId, _Value, SubTree} | RestTree], KeyPathAcc) ->
14    get_all_leafs_simple(Pos + 1, SubTree, [KeyId | KeyPathAcc]) ++ get_all_leafs_simple(Pos, RestTree, KeyPathAcc).

[{Value, {Pos, [KeyId, ...]}}, ...]

というleafの集合が返ってきます。それを

[{{Pos, RevId}, {Deleted, Sp, Revs}}, ...]

というリストに変換した後、dict:from_list/1でdictionaryに変換します。それをFullDocInfoと共にprep_and_validate_update/5に渡してバリデーションチェック等を行い、ドキュメントが更新OKかNGかを判定しているようです。not_foundが指定された場合と比較すると、OldFullDocInfoからleafの値を取り出して、バリデーションチェックを行っているであろうprep_and_validate_update/5に渡しているところが異なります。prep_and_validate_update/5を見てみます。

couch_db.erl:

 1prep_and_validate_update(Db, #doc{id=Id,revs={RevStart, Revs}}=Doc,
 2        OldFullDocInfo, LeafRevsDict, AllowConflict) ->
 3    case Revs of
 4    [PrevRev|_] ->
 5        case dict:find({RevStart, PrevRev}, LeafRevsDict) of
 6        {ok, {Deleted, DiskSp, DiskRevs}} ->
 7            case couch_doc:has_stubs(Doc) of
 8            true ->
 9                DiskDoc = make_doc(Db, Id, Deleted, DiskSp, DiskRevs),
10                Doc2 = couch_doc:merge_stubs(Doc, DiskDoc),
11                {validate_doc_update(Db, Doc2, fun() -> DiskDoc end), Doc2};
12            false ->
13                LoadDiskDoc = fun() -> make_doc(Db,Id,Deleted,DiskSp,DiskRevs) end,
14                {validate_doc_update(Db, Doc, LoadDiskDoc), Doc}
15            end;
16        error when AllowConflict ->
17            couch_doc:merge_stubs(Doc, #doc{}), % will generate error if
18                                                        % there are stubs
19            {validate_doc_update(Db, Doc, fun() -> nil end), Doc};
20        error ->
21            {conflict, Doc}
22        end;
23    [] ->
24        % new doc, and we have existing revs.
25        % reuse existing deleted doc
26        if OldFullDocInfo#full_doc_info.deleted orelse AllowConflict ->
27            {validate_doc_update(Db, Doc, fun() -> nil end), Doc};
28        true ->
29            {conflict, Doc}
30        end
31    end.

DocからRevsを取り出し、要素が1つ以上設定されているかによって処理を分けています。Revsに要素が1つ以上設定されている場合は、その先頭の要素をPrevRevにバインドし、LeafRevsDict{RevStart, PrevRev}で検索します。{RevStart, PrevRev}は、{Pos, RevId(KeyId)}なので、更新対象のDocOldFullDocInfoのleafに含まれているかどうかチェックしているようです。含まれていれば先程と同様にcouch_doc:has_stubs/1を呼び出し、必要があればcouch_doc:merge_stubs/2を呼び出してアタッチメントをマージした上で、validate_doc_update/3の結果を返すようになっています。

couch_db:validate_doc_update/3

couch_db:validate_doc_update/3を見てみます。

couch_db.erl:

 1validate_doc_update(#db{}=Db, #doc{id= <<"_design/",_/binary>>}, _GetDiskDocFun) ->
 2    catch check_is_admin(Db);
 3validate_doc_update(#db{validate_doc_funs=[]}, _Doc, _GetDiskDocFun) ->
 4    ok;
 5validate_doc_update(_Db, #doc{id= <<"_local/",_/binary>>}, _GetDiskDocFun) ->
 6    ok;
 7validate_doc_update(Db, Doc, GetDiskDocFun) ->
 8    DiskDoc = GetDiskDocFun(),
 9    JsonCtx = couch_util:json_user_ctx(Db),
10    SecObj = get_security(Db),
11    try [case Fun(Doc, DiskDoc, JsonCtx, SecObj) of
12            ok -> ok;
13            Error -> throw(Error)
14        end || Fun <- Db#db.validate_doc_funs],
15        ok
16    catch
17        throw:Error ->
18            Error
19    end.

更新対象のDocDesignDocの場合はadminかどうかチェックしているようです。#db.validate_doc_funsにvalidation用の関数が設定されていない場合、またはDoc#doc.idがlocalのものであればvalidationは実施されません。

上記以外の場合、まずGetDiskDocFunsを実行してDiskDocを取得します。このGetDiskDocFuns以前みたようにデータファイルからDocのデータを取得する関数です。DiskDocOldFullDocInfoに保存されているポインタから取得したドキュメントデータなので、更新ドキュメントの一つ前のバージョンのドキュメントデータになります。

JsonCtx及びSecObjを取得し、Db#db.validate_doc_funsに設定されているvalidation関数に対して(Doc, DiskDoc, JsonCtx, SecObj)を渡してvalidationを実行し、すべてOKだった場合はokが、そうでない場合はvalidation関数が返すErrorで返るようになっています。

ちなみにvalidate_update_docという関数名は、「CouchDB Source Code Reading Part5#couchdocgetvalidatedocfun1」で触れてますが、あちらはcouch_query_serversモジュールで定義されている同名の関数で、恐らく同じようなことをやっているのだと思います。

strip out any empty buckets

couch_db:validate_doc_update/3を使ってドキュメントのvalidationを行い、DocBucket毎に{PreppedBucket, AccErrors3}に分けられます。PreppedBucketはvalidationした結果OKだったドキュメントのリスト、AccErrrosはNGだったドキュメントのリストです。DocBucketsリストにあるすべてのDocBucketに対してこれらの分類が終わった後、couch_db:update_docs/4では以下のようなコードを通ります。

couch_db.erl:

1% strip out any empty buckets
2DocBuckets2 = [Bucket || [_|_] = Bucket <- DocBucketsPrepped];                                                                                      

コメントを見ると、空Bucketを取り除く、という意図に読めるのですが、このコードの意味が良く分からなかったので、ここだけ試してみました。

 1Eshell V5.10.3  (abort with ^G)
 21> A=[1,[2,3],4].
 3[1,[2,3],4]
 42> B=[X || [_|_]=X <- A].
 5[[2,3]]
 63> B.
 7[[2,3]]
 84> A2=[1,[2],3,4].
 9[1,[2],3,4]
105> B2=[X || [_|_]=X <- A2].
11[[2]]
126> A3=[[1],[],[3],[4]].
13[[1],[],[3],[4]]
147> B3=[X || [_|_]=X <- A3].
15[[1],[3],[4]]

ABの結果を見ると、リストの中のリスト([2,3])がピックアップされているのが分かります。また、A2B2の結果から、リストの中のリストの要素数は1でも問題ないことが分かります。そしてA3B3の結果から、リストの中のリストの要素数が0のものは取り除かれているのが分かります。これらの結果から、コメントにあるように空Bucketを取り除く、という意図であることが分かりました。

validationを実行した結果、DocBucketの中のすべてのDocがOK、もしくはすべてのDocがNGだった場合、もう一方のバケットは空になる為、それを取り除く意図なのだと思います。Aから14が除去されてBに格納される部分について、X14の場合、パターンマッチのところで数値とリストで型が合わないのでエラーになるのかと思ったのですが、14が自動的に除去されるのがちょっと意外でした。リスト内包表記は、パターンマッチしないものは結果リストに含まれない仕様なんですね。

Conclusion

ドキュメント更新前のvalidationの呼び出し部分を見てきました。データ構造を組み替えたり、DiskDocが入ってくるなど複雑で読みにくい感じがします。また、リスト内包表記は、パターンマッチしないものはエラーにならず結果のリストに含まれない、ということを初めて知りました。