couch_db:open_doc_int/3
前回はcouch_db_updater:refresh_validate_doc_funs/1
を読んでいく中でDesignDocs
を取得するシーケンスを追っていきました。今回はそこからの続き。
couch_db_updater.erl:
1refresh_validate_doc_funs(Db0) ->
2 Db = Db0#db{user_ctx = #user_ctx{roles=[<<"_admin">>]}},
3 DesignDocs = couch_db:get_design_docs(Db),
4 ProcessDocFuns = lists:flatmap(
5 fun(DesignDocInfo) ->
6 {ok, DesignDoc} = couch_db:open_doc_int(
7 Db, DesignDocInfo, [ejson_body]),
8 case couch_doc:get_validate_doc_fun(DesignDoc) of
9 nil -> [];
10 Fun -> [Fun]
11 end
12 end, DesignDocs),
13 Db0#db{validate_doc_funs=ProcessDocFuns}.
DesignDocs
を取得した後、lists:flatmap/2
を使ってDesignDocs
の各要素から関数のリストを取得しています。lists:flatmap/2
は文字通りmapした結果をflattenしてくれる関数です。以下のような動きをします。
11> lists:flatmap(fun(E) -> [E, E*2] end, [1, 2, 3]).
2[1,2,2,4,3,6]
Fun
が単体の関数か、関数のリストが返る、ということですかね。
couch_db:open_doc_int/3
まずはcouch_db:open_doc_int/3
を見てみます。
couch_db.erl
1-
2open_doc_int(Db, <<?LOCAL_DOC_PREFIX, _/binary>> = Id, Options) ->
3 case couch_btree:lookup(local_btree(Db), [Id]) of
4 [{ok, {_, {Rev, BodyData}}}] ->
5 Doc = #doc{id=Id, revs={0, [?l2b(integer_to_list(Rev))]}, body=BodyData},
6 apply_open_options({ok, Doc}, Options);
7 [not_found] ->
8 {not_found, missing}
9 end;
10open_doc_int(Db, #doc_info{id=Id,revs=[RevInfo|_]}=DocInfo, Options) ->
11 #rev_info{deleted=IsDeleted,rev={Pos,RevId},body_sp=Bp} = RevInfo,
12 Doc = make_doc(Db, Id, IsDeleted, Bp, {Pos,[RevId]}),
13 apply_open_options(
14 {ok, Doc#doc{meta=doc_meta_info(DocInfo, [], Options)}}, Options);
15open_doc_int(Db, #full_doc_info{id=Id,rev_tree=RevTree}=FullDocInfo, Options) ->
16 #doc_info{revs=[#rev_info{deleted=IsDeleted,rev=Rev,body_sp=Bp}|_]} =
17 DocInfo = couch_doc:to_doc_info(FullDocInfo),
18 {[{_, RevPath}], []} = couch_key_tree:get(RevTree, [Rev]),
19 Doc = make_doc(Db, Id, IsDeleted, Bp, RevPath),
20 apply_open_options(
21 {ok, Doc#doc{meta=doc_meta_info(DocInfo, RevTree, Options)}}, Options);
22open_doc_int(Db, Id, Options) ->
23 case get_full_doc_info(Db, Id) of
24 {ok, FullDocInfo} ->
25 open_doc_int(Db, FullDocInfo, Options);
26 not_found ->
27 {not_found, missing}
引数に指定されるDesignDocs
の各要素は#full_doc_info
なので、上から3つ目の関数にマッチするはずです。その#full_doc_info
をcouch_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:
1get(Tree, KeysToGet) ->
2 {KeyPaths, KeysNotFound} = get_full_key_paths(Tree, KeysToGet),
3 FixedResults = [ {Value, {Pos, [Key0 || {Key0, _} <- Path]}} || {Pos, [{_Key, Value}|_]=Path} <- KeyPaths],
4 {FixedResults, KeysNotFound}.
5
6get_full_key_paths(Tree, Keys) ->
7 get_full_key_paths(Tree, Keys, []).
8
9get_full_key_paths(_, [], Acc) ->
10 {Acc, []};
11get_full_key_paths([], Keys, Acc) ->
12 {Acc, Keys};
13get_full_key_paths([{Pos, Tree}|Rest], Keys, Acc) ->
14 {Gotten, RemainingKeys} = get_full_key_paths(Pos, [Tree], Keys, []),
15 get_full_key_paths(Rest, RemainingKeys, Gotten ++ Acc).
16
17get_full_key_paths(_Pos, _Tree, [], _KeyPathAcc) ->
18 {[], []};
19get_full_key_paths(_Pos, [], KeysToGet, _KeyPathAcc) ->
20 {[], KeysToGet};
21get_full_key_paths(Pos, [{KeyId, Value, SubTree} | RestTree], KeysToGet, KeyPathAcc) ->
22 KeysToGet2 = KeysToGet -- [{Pos, KeyId}],
23 CurrentNodeResult =
24 case length(KeysToGet2) =:= length(KeysToGet) of
25 true -> % not in the key list.
26 [];
27 false -> % this node is the key list. return it
28 [{Pos, [{KeyId, Value} | KeyPathAcc]}]
29 end,
30 {KeysGotten, KeysRemaining} = get_full_key_paths(Pos + 1, SubTree, KeysToGet2, [{KeyId, Value} | KeyPathAcc]),
31 {KeysGotten2, KeysRemaining2} = get_full_key_paths(Pos, RestTree, KeysRemaining, KeyPathAcc),
32 {CurrentNodeResult ++ KeysGotten ++ KeysGotten2, KeysRemaining2}.
指定された
KeyToGet([{Pos, KeyId},...])`
に合致するValue
を取得しています。SubTree
も再帰的に探しに行っているのですが、その際にKeyPathAcc
にはParentTreeでKeyが合致した時の結果を含めています。その為、FixedResult
の
[ {Value, {Pos, [Key,...]}}, ... ]
で一つのValue
に複数のKey
が関連付けられているのは、KeyToGet
に合致したValue
のKey
だけでなく、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:
1read_doc(#db{fd=Fd}, Pos) ->
2 couch_file:pread_term(Fd, Pos).
3
4make_doc(#db{updater_fd = Fd} = Db, Id, Deleted, Bp, RevisionPath) ->
5 {BodyData, Atts} =
6 case Bp of
7 nil ->
8 {[], []};
9 _ ->
10 {ok, {BodyData0, Atts00}} = read_doc(Db, Bp),
11 Atts0 = case Atts00 of
12 _ when is_binary(Atts00) ->
13 couch_compress:decompress(Atts00);
14 _ when is_list(Atts00) ->
15 % pre 1.2 format
16 Atts00
17 end,
18 {BodyData0,
19 lists:map(
20 fun({Name,Type,Sp,AttLen,DiskLen,RevPos,Md5,Enc}) ->
21 #att{name=Name,
22 type=Type,
23 att_len=AttLen,
24 disk_len=DiskLen,
25 md5=Md5,
26 revpos=RevPos,
27 data={Fd,Sp},
28 encoding=
29 case Enc of
30 true ->
31 % 0110 UPGRADE CODE
32 gzip;
33 false ->
34 % 0110 UPGRADE CODE
35 identity;
36 _ ->
37 Enc
38 end
39 };
40 ({Name,Type,Sp,AttLen,RevPos,Md5}) ->
41 #att{name=Name,
42 type=Type,
43 att_len=AttLen,
44 disk_len=DiskLen,
45 md5=Md5,
46 revpos=RevPos,
47 data={Fd,Sp},
48 encoding=
49 case Enc of
50 true ->
51 % 0110 UPGRADE CODE
52 gzip;
53 false ->
54 % 0110 UPGRADE CODE
55 identity;
56 _ ->
57 Enc
58 end
59 };
60 ({Name,Type,Sp,AttLen,RevPos,Md5}) ->
61 #att{name=Name,
62 type=Type,
63 att_len=AttLen,
64 disk_len=AttLen,
65 md5=Md5,
66 revpos=RevPos,
67 data={Fd,Sp}};
68 ({Name,{Type,Sp,AttLen}}) ->
69 #att{name=Name,
70 type=Type,
71 att_len=AttLen,
72 disk_len=AttLen,
73 md5= <<>>,
74 revpos=0,
75 data={Fd,Sp}}
76 end, Atts0)}
77 end,
78 Doc = #doc{
79 id = Id,
80 revs = RevisionPath,
81 body = BodyData,
82 atts = Atts,
83 deleted = Deleted
84 },
85 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:
1after_doc_read(#db{after_doc_read = nil}, Doc) ->
2 Doc;
3after_doc_read(#db{after_doc_read = Fun} = Db, Doc) ->
4 Fun(couch_doc:with_ejson_body(Doc), Db).
#db.after_doc_read
に関数が設定されていれば、その関数を実行するようになっています。そのままcouch_doc:with_ejson_body/1
を読んでみます。
couch_doc.erl:
1with_ejson_body(#doc{body = Body} = Doc) when is_binary(Body) ->
2 Doc#doc{body = couch_compress:decompress(Body)};
3with_ejson_body(#doc{body = {_}} = Doc) ->
4 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:
1apply_open_options({ok, Doc},Options) ->
2 apply_open_options2(Doc,Options);
3apply_open_options(Else,_Options) ->
4 Else.
5
6apply_open_options2(Doc,[]) ->
7 {ok, Doc};
8apply_open_options2(#doc{atts=Atts,revs=Revs}=Doc,
9 [{atts_since, PossibleAncestors}|Rest]) ->
10 RevPos = find_ancestor_rev_pos(Revs, PossibleAncestors),
11 apply_open_options2(Doc#doc{atts=[A#att{data=
12 if AttPos>RevPos -> Data; true -> stub end}
13 || #att{revpos=AttPos,data=Data}=A <- Atts]}, Rest);
14apply_open_options2(Doc, [ejson_body | Rest]) ->
15 apply_open_options2(couch_doc:with_ejson_body(Doc), Rest);
16apply_open_options2(Doc,[_|Rest]) ->
17 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:
1get_validate_doc_fun(#doc{body={Props}}=DDoc) ->
2 case couch_util:get_value(<<"validate_doc_update">>, Props) of
3 undefined ->
4 nil;
5 _Else ->
6 fun(EditDoc, DiskDoc, Ctx, SecObj) ->
7 couch_query_servers:validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx, SecObj)
8 end
9 end.
#doc.body
の{Props}
に<<"validate_doc_update">>
が設定されているかどうか見ています。Props
の構造がすなわち#doc.body
の構造となるので、couch_util:get_value/2
を読んでみます。
couch_util.erl:
1get_value(Key, List) ->
2 get_value(Key, List, undefined).
3
4get_value(Key, List, Default) ->
5 case lists:keysearch(Key, 1, List) of
6 {value, {Key,Value}} ->
7 Value;
8 false ->
9 Default
10 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% use the function stored in ddoc.validate_doc_update to test an update.
2validate_doc_update(DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
3 JsonEditDoc = couch_doc:to_json_obj(EditDoc, [revs]),
4 JsonDiskDoc = json_doc(DiskDoc),
5 case ddoc_prompt(DDoc, [<<"validate_doc_update">>], [JsonEditDoc, JsonDiskDoc, Ctx, SecObj]) of
6 1 ->
7 ok;
8 {[{<<"forbidden">>, Message}]} ->
9 throw({forbidden, Message});
10 {[{<<"unauthorized">>, Message}]} ->
11 throw({unauthorized, Message})
12 end.
EditDoc
やDiskDoc
とか良く分かりませんが、<<"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
はドキュメントに関するメタ情報のようなものなのではないかと思います。