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).
ドキュメントのアタッチメントのdata
がstub
のものが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{}
なので、DiskBins
はundefined
になるんじゃないかな…。呼び出し側のコメントにも
% 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)}
なので、更新対象のDoc
がOldFullDocInfo
の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.
更新対象のDoc
がDesignDoc
の場合はadminかどうかチェックしているようです。#db.validate_doc_funs
にvalidation用の関数が設定されていない場合、またはDoc#doc.id
がlocalのものであればvalidationは実施されません。
上記以外の場合、まずGetDiskDocFuns
を実行してDiskDoc
を取得します。このGetDiskDocFuns
は以前みたようにデータファイルからDocのデータを取得する関数です。DiskDoc
はOldFullDocInfo
に保存されているポインタから取得したドキュメントデータなので、更新ドキュメントの一つ前のバージョンのドキュメントデータになります。
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]]
A
とB
の結果を見ると、リストの中のリスト([2,3]
)がピックアップされているのが分かります。また、A2
とB2
の結果から、リストの中のリストの要素数は1でも問題ないことが分かります。そしてA3
とB3
の結果から、リストの中のリストの要素数が0のものは取り除かれているのが分かります。これらの結果から、コメントにあるように空Bucketを取り除く、という意図であることが分かりました。
validationを実行した結果、DocBucket
の中のすべてのDoc
がOK、もしくはすべてのDoc
がNGだった場合、もう一方のバケットは空になる為、それを取り除く意図なのだと思います。A
から1
と4
が除去されてB
に格納される部分について、X
が1
や4
の場合、パターンマッチのところで数値とリストで型が合わないのでエラーになるのかと思ったのですが、1
と4
が自動的に除去されるのがちょっと意外でした。リスト内包表記は、パターンマッチしないものは結果リストに含まれない仕様なんですね。
Conclusion
ドキュメント更新前のvalidationの呼び出し部分を見てきました。データ構造を組み替えたり、DiskDoc
が入ってくるなど複雑で読みにくい感じがします。また、リスト内包表記は、パターンマッチしないものはエラーにならず結果のリストに含まれない、ということを初めて知りました。