act-act about projects rss

CouchDB Source Code Reading part5

couch_db:open_doc_int/3

前回はcouch_db_updater:refresh_validate_doc_funs/1を読んでいく中でDesignDocsを取得するシーケンスを追っていきました。今回はそこからの続き。

couch_db_updater.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
refresh_validate_doc_funs(Db0) ->
    Db = Db0#db{user_ctx = #user_ctx{roles=[<<"_admin">>]}},
    DesignDocs = couch_db:get_design_docs(Db),
    ProcessDocFuns = lists:flatmap(
        fun(DesignDocInfo) ->
            {ok, DesignDoc} = couch_db:open_doc_int(
                Db, DesignDocInfo, [ejson_body]),
            case couch_doc:get_validate_doc_fun(DesignDoc) of
            nil -> [];
            Fun -> [Fun]
            end
        end, DesignDocs),
    Db0#db{validate_doc_funs=ProcessDocFuns}.

DesignDocsを取得した後、lists:flatmap/2を使ってDesignDocsの各要素から関数のリストを取得しています。lists:flatmap/2は文字通りmapした結果をflattenしてくれる関数です。以下のような動きをします。

1
2
1> lists:flatmap(fun(E) -> [E, E*2] end, [1, 2, 3]).
[1,2,2,4,3,6]

Funが単体の関数か、関数のリストが返る、ということですかね。

couch_db:open_doc_int/3

まずはcouch_db:open_doc_int/3を見てみます。

couch_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-
open_doc_int(Db, <<?LOCAL_DOC_PREFIX, _/binary>> = Id, Options) ->
    case couch_btree:lookup(local_btree(Db), [Id]) of
    [{ok, {_, {Rev, BodyData}}}] ->
        Doc = #doc{id=Id, revs={0, [?l2b(integer_to_list(Rev))]}, body=BodyData},
        apply_open_options({ok, Doc}, Options);
    [not_found] ->
        {not_found, missing}
    end;
open_doc_int(Db, #doc_info{id=Id,revs=[RevInfo|_]}=DocInfo, Options) ->
    #rev_info{deleted=IsDeleted,rev={Pos,RevId},body_sp=Bp} = RevInfo,
    Doc = make_doc(Db, Id, IsDeleted, Bp, {Pos,[RevId]}),
    apply_open_options(
       {ok, Doc#doc{meta=doc_meta_info(DocInfo, [], Options)}}, Options);
open_doc_int(Db, #full_doc_info{id=Id,rev_tree=RevTree}=FullDocInfo, Options) ->
    #doc_info{revs=[#rev_info{deleted=IsDeleted,rev=Rev,body_sp=Bp}|_]} =
        DocInfo = couch_doc:to_doc_info(FullDocInfo),
    {[{_, RevPath}], []} = couch_key_tree:get(RevTree, [Rev]),
    Doc = make_doc(Db, Id, IsDeleted, Bp, RevPath),
    apply_open_options(
        {ok, Doc#doc{meta=doc_meta_info(DocInfo, RevTree, Options)}}, Options);
open_doc_int(Db, Id, Options) ->
    case get_full_doc_info(Db, Id) of
    {ok, FullDocInfo} ->
        open_doc_int(Db, FullDocInfo, Options);
    not_found ->
        {not_found, missing}

引数に指定されるDesignDocsの各要素は#full_doc_infoなので、上から3つ目の関数にマッチするはずです。その#full_doc_infocouch_doc:to_doc_info/1#doc_infoに変換し、#full_doc_info.rev_treeから#doc_info.revsの先頭のrevにマッチする要素を取得しています。

couch_key_tree:get/2

[rev]にマッチする要素の取得まわりを実行しているる関数couch_key_tree:get/2を見てみます。

couch_key_tree.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
get(Tree, KeysToGet) ->
    {KeyPaths, KeysNotFound} = get_full_key_paths(Tree, KeysToGet),
    FixedResults = [ {Value, {Pos, [Key0 || {Key0, _} <- Path]}} || {Pos, [{_Key, Value}|_]=Path} <- KeyPaths],
    {FixedResults, KeysNotFound}.

get_full_key_paths(Tree, Keys) ->
    get_full_key_paths(Tree, Keys, []).

get_full_key_paths(_, [], Acc) ->
    {Acc, []};
get_full_key_paths([], Keys, Acc) ->
    {Acc, Keys};
get_full_key_paths([{Pos, Tree}|Rest], Keys, Acc) ->
    {Gotten, RemainingKeys} = get_full_key_paths(Pos, [Tree], Keys, []),
    get_full_key_paths(Rest, RemainingKeys, Gotten ++ Acc).

get_full_key_paths(_Pos, _Tree, [], _KeyPathAcc) ->
    {[], []};
get_full_key_paths(_Pos, [], KeysToGet, _KeyPathAcc) ->
    {[], KeysToGet};
get_full_key_paths(Pos, [{KeyId, Value, SubTree} | RestTree], KeysToGet, KeyPathAcc) ->
    KeysToGet2 = KeysToGet -- [{Pos, KeyId}],
    CurrentNodeResult =
    case length(KeysToGet2) =:= length(KeysToGet) of
    true -> % not in the key list.
        [];
    false -> % this node is the key list. return it
        [{Pos, [{KeyId, Value} | KeyPathAcc]}]
    end,
    {KeysGotten, KeysRemaining} = get_full_key_paths(Pos + 1, SubTree, KeysToGet2, [{KeyId, Value} | KeyPathAcc]),
    {KeysGotten2, KeysRemaining2} = get_full_key_paths(Pos, RestTree, KeysRemaining, KeyPathAcc),
    {CurrentNodeResult ++ KeysGotten ++ KeysGotten2, KeysRemaining2}.

指定されたKeyToGet([{Pos, KeyId},...])に合致するValueを取得しています。SubTreeも再帰的に探しに行っているのですが、その際にKeyPathAccにはParentTreeでKeyが合致した時の結果を含めています。その為、FixedResult[ {Value, {Pos, [Key,...]}}, ... ]で一つのValueに複数のKeyが関連付けられているのは、KeyToGetに合致したValueKeyだけでなく、ParentTreeのKeyを含んでいることになります。この複数のKeyがValueのノードのパスを表しています。

couch_db:make_doc/5

couch_db:open_doc_int/1に戻って、前記の内容から、RevPath{Pos, [Key, ...]}になるはずです。このRevPathを指定して呼び出しているmake_doc/5を見てみます。

couch_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
read_doc(#db{fd=Fd}, Pos) ->
    couch_file:pread_term(Fd, Pos).

make_doc(#db{updater_fd = Fd} = Db, Id, Deleted, Bp, RevisionPath) ->
    {BodyData, Atts} =
    case Bp of
    nil ->
        {[], []};
    _ ->
        {ok, {BodyData0, Atts00}} = read_doc(Db, Bp),
        Atts0 = case Atts00 of
        _ when is_binary(Atts00) ->
            couch_compress:decompress(Atts00);
        _ when is_list(Atts00) ->
            % pre 1.2 format
            Atts00
        end,
        {BodyData0,
            lists:map(
                fun({Name,Type,Sp,AttLen,DiskLen,RevPos,Md5,Enc}) ->
                    #att{name=Name,
                        type=Type,
                        att_len=AttLen,
                        disk_len=DiskLen,
                        md5=Md5,
                        revpos=RevPos,
                        data={Fd,Sp},
                        encoding=
                            case Enc of
                            true ->
                                % 0110 UPGRADE CODE
                                gzip;
                            false ->
                                % 0110 UPGRADE CODE
                                identity;
                            _ ->
                                Enc
                            end
                    };
                ({Name,Type,Sp,AttLen,RevPos,Md5}) ->
                    #att{name=Name,
                        type=Type,
                        att_len=AttLen,
                        disk_len=DiskLen,
                        md5=Md5,
                        revpos=RevPos,
                        data={Fd,Sp},
                        encoding=
                            case Enc of
                            true ->
                                % 0110 UPGRADE CODE
                                gzip;
                            false ->
                                % 0110 UPGRADE CODE
                                identity;
                            _ ->
                                Enc
                            end
                    };
                ({Name,Type,Sp,AttLen,RevPos,Md5}) ->
                    #att{name=Name,
                        type=Type,
                        att_len=AttLen,
                        disk_len=AttLen,
                        md5=Md5,
                        revpos=RevPos,
                        data={Fd,Sp}};
                ({Name,{Type,Sp,AttLen}}) ->
                    #att{name=Name,
                        type=Type,
                        att_len=AttLen,
                        disk_len=AttLen,
                        md5= <<>>,
                        revpos=0,
                        data={Fd,Sp}}
                end, Atts0)}
    end,
    Doc = #doc{
        id = Id,
        revs = RevisionPath,
        body = BodyData,
        atts = Atts,
        deleted = Deleted
    },
    after_doc_read(Db, Doc).

かなり長いのですが、変換後の#doc_info.revsの先頭要素のBodyPointerを指定してread_doc/2を呼び出してデータファイルから{BodyData, Atts}を取得し、#docに入れて返しています。これまで#full_doc_info.rev_treeに保存されているValueは実値ではなくポインタを保持しているだけでしたが、この関数を呼び出すことによって実値をデータファイルから取り出していることになります。lists:map/2の関数で複数のパターンが用意されているのは、恐らくバージョンによって保持している属性情報の内容が異なるからでしょう。

そのままafter_doc_read/2を見てみます。

couch_db.erl

1
2
3
4
after_doc_read(#db{after_doc_read = nil}, Doc) ->
    Doc;
after_doc_read(#db{after_doc_read = Fun} = Db, Doc) ->
    Fun(couch_doc:with_ejson_body(Doc), Db).

#db.after_doc_readに関数が設定されていれば、その関数を実行するようになっています。そのままcouch_doc:with_ejson_body/1を読んでみます。

couch_doc.erl

1
2
3
4
with_ejson_body(#doc{body = Body} = Doc) when is_binary(Body) ->
    Doc#doc{body = couch_compress:decompress(Body)};
with_ejson_body(#doc{body = {_}} = Doc) ->
    Doc.

#doc.bodyがbinaryであればdecompressしていました。

couch_db:open_doc_int/3に戻って、doc_meta_info/3ですが、Optionsに指定されているのが[ejson_body]だけなので[]が返ります(コードは省略します)。次にapply_open_options/2を読んでみます。

couch_db.erl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apply_open_options({ok, Doc},Options) ->
    apply_open_options2(Doc,Options);
apply_open_options(Else,_Options) ->
    Else.

apply_open_options2(Doc,[]) ->
    {ok, Doc};
apply_open_options2(#doc{atts=Atts,revs=Revs}=Doc,
        [{atts_since, PossibleAncestors}|Rest]) ->
    RevPos = find_ancestor_rev_pos(Revs, PossibleAncestors),
    apply_open_options2(Doc#doc{atts=[A#att{data=
        if AttPos>RevPos -> Data; true -> stub end}
        || #att{revpos=AttPos,data=Data}=A <- Atts]}, Rest);
apply_open_options2(Doc, [ejson_body | Rest]) ->
    apply_open_options2(couch_doc:with_ejson_body(Doc), Rest);
apply_open_options2(Doc,[_|Rest]) ->
    apply_open_options2(Doc,Rest).

Options[ejson_body]なので、couch_doc:with_ejson_body/1を呼び出して適宜decompressして終了です。以上でcouch_db:open_doc_int/3は終わりです。

couch_doc:get_validate_doc_fun/1

couch_db_updater:refresh_validate_doc_funs/1に戻り、couch_doc:get_validate_doc_fun/1を見てみます。

couch_doc.erl

1
2
3
4
5
6
7
8
9
get_validate_doc_fun(#doc{body={Props}}=DDoc) ->
    case couch_util:get_value(<<"validate_doc_update">>, Props) of
    undefined ->
        nil;
    _Else ->
        fun(EditDoc, DiskDoc, Ctx, SecObj) ->
            couch_query_servers:validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx, SecObj)
        end
    end.

#doc.body{Props}<<"validate_doc_update">>が設定されているかどうか見ています。Propsの構造がすなわち#doc.bodyの構造となるので、couch_util:get_value/2を読んでみます。

couch_util.erl

1
2
3
4
5
6
7
8
9
10
get_value(Key, List) ->
    get_value(Key, List, undefined).

get_value(Key, List, Default) ->
    case lists:keysearch(Key, 1, List) of
    {value, {Key,Value}} ->
        Value;
    false ->
        Default
    end.

lists/keysearch/3を使って値を取っています。ErlangのSTDLIB Reference Manualを見ると、lists:keysearch/3の第三引数はTupleListとなっているので、タプルのリストが#doc.bodyに設定されていることになります。

そのままcouch_query_servers:validate_doc_update/5を見てみます。

couch_query_servers.erl

1
2
3
4
5
6
7
8
9
10
11
12
% use the function stored in ddoc.validate_doc_update to test an update.
validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
    JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]),
    JsonDiskDoc = json_doc(DiskDoc),
    case ddoc_prompt(DDoc, [<<"validate_doc_update">>], [JsonEditDoc, JsonDiskDoc, Ctx, SecObj]) of
        1 ->
            ok;
        {[{<<"forbidden">>, Message}]} ->
            throw({forbidden, Message});
        {[{<<"unauthorized">>, Message}]} ->
            throw({unauthorized, Message})
    end.

EditDocDiskDocとか良く分かりませんが、<<"forbidden">><<"unauthorized">>とあることから、ドキュメントの権限まわりのチェックを行う関数のようです。

Conclusion

今回はcouch_db_updater:refresh_validate_doc_funs/1を読んでいく中で、#full_doc_info -> #doc_info -> #docの取得の流れを見てきました。#doc_info#docの違いが分かりました。

そして、DesignDocの中に<<"validate_doc_update">>が設定されているかどうかでドキュメントのvalidation関数を設定していることから、DesignDocはドキュメントに関するメタ情報のようなものなのではないかと思います。