├── README.md ├── hovercraft.erl └── hovercraft_test.erl /README.md: -------------------------------------------------------------------------------- 1 | We have the hovercraft. It is fast and it skims just above the surface. 2 | 3 | There is much to be added to the hovercraft, but it already flies. 4 | 5 | ## Welcome to Hovercraft 6 | 7 | An easy direct Erlang CouchDB library. 8 | 9 | Use this to abstract CouchDB behind a simple Erlang function call. Currently supports the database and document APIs, with views on the way. 10 | 11 | ## Basic Usage 12 | 13 | Hovercraft master is only compatible with CouchDB trunk. If the tests fail, 14 | make sure you are running on the latest latest CouchDB. 15 | 16 | There is a 0.11 branch which is compatible with CouchDB 0.11. Use it when appropriate. 17 | 18 | The easiest way to try Hovercraft is to put the hovercraft directory 19 | inside the CouchDB trunk directory and then launch CouchDB like this: 20 | 21 | erlc hovercraft/*erl && make dev && ERL_LIBS="hovercraft" utils/run -i 22 | 23 | This will open an interactive session. To run the tests, call 24 | hovercraft:test/0 like this: 25 | 26 | 1> hovercraft_test:all(). 27 | [info] [<0.30.0>] Starting tests in <<"hovercraft-test">> 28 | ok 29 | 30 | ## Speed of Light 31 | 32 | To run the speed of light test, run hovercraft:lightning/0 like this: 33 | 34 | 2> hovercraft_test:lightning(). 35 | Inserted 100000 docs in 14.967256 seconds with batch size of 1000. (6681.251393040915 docs/sec) 36 | ok 37 | 38 | To try different tunings, you can call hovercraft:lightning/1 with 39 | custom batch sizes. The docs in the speed of light test are small, feel 40 | free to edit the source code to try larger docs. 41 | 42 | ## Credits 43 | 44 | Released at #CouchHack '09 45 | Apache 2.0 License 46 | Copyright 2009 J. Chris Anderson 47 | -------------------------------------------------------------------------------- /hovercraft.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% File : hovercraft.erl 3 | %%% Author : J Chris Anderson 4 | %%% Description : Erlang CouchDB access. 5 | %%% 6 | %%% Created : 4 Apr 2009 by J Chris Anderson 7 | %%% License : Apache 2.0 8 | %%%------------------------------------------------------------------- 9 | -module(hovercraft). 10 | -vsn("0.1.4"). 11 | 12 | %% see README.md for usage information 13 | 14 | %% Database API 15 | -export([ 16 | create_db/1, 17 | delete_db/1, 18 | open_db/1, 19 | db_info/1, 20 | open_doc/2, 21 | save_doc/2, 22 | save_bulk/2, 23 | delete_doc/2, 24 | start_attachment/3, 25 | next_attachment_bytes/1, 26 | query_view/3, 27 | query_view/4, 28 | query_view/5 29 | ]). 30 | 31 | % Hovercraft is designed to run inside the same beam process as CouchDB. 32 | % It calls into CouchDB's database access functions, so it needs access 33 | % to the disk. To hook it up, make sure hovercraft is in the Erlang load 34 | % path and ensure that the following line points to CouchDB's hrl file. 35 | -include_lib("src/couchdb/couch_db.hrl"). 36 | 37 | -define(ADMIN_USER_CTX, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). 38 | 39 | %%==================================================================== 40 | %% API 41 | %%==================================================================== 42 | %%-------------------------------------------------------------------- 43 | %% Function: create_db(DbName) -> {ok,created} | {error,Error} 44 | %% DbName is always a binary: <<"mydb">> 45 | %% Description: Creates the database. 46 | %%-------------------------------------------------------------------- 47 | 48 | create_db(DbName) -> 49 | create_db(DbName, []). 50 | 51 | create_db(DbName, Options) -> 52 | case couch_server:create(DbName, Options) of 53 | {ok, Db} -> 54 | couch_db:close(Db), 55 | {ok, created}; 56 | Error -> 57 | {error, Error} 58 | end. 59 | 60 | %%-------------------------------------------------------------------- 61 | %% Function: delete_db(DbName) -> {ok,deleted} | {error,Error} 62 | %% Description: Deletes the database. 63 | %%-------------------------------------------------------------------- 64 | delete_db(DbName) -> 65 | delete_db(DbName, [?ADMIN_USER_CTX]). 66 | 67 | delete_db(DbName, Options) -> 68 | case couch_server:delete(DbName, Options) of 69 | ok -> 70 | {ok, deleted}; 71 | Error -> 72 | {error, Error} 73 | end. 74 | 75 | %%-------------------------------------------------------------------- 76 | %% Function: open_db(DbName) -> {ok,Db} | {error,Error} 77 | %% Description: Opens the database. 78 | %% Only use this if you are doing bulk operations and know what you are doing. 79 | %%-------------------------------------------------------------------- 80 | open_db(DbName) -> 81 | couch_db:open(DbName, [?ADMIN_USER_CTX]). 82 | 83 | %%-------------------------------------------------------------------- 84 | %% Function: db_info(DbName) -> {ok,Db} | {error,Error} 85 | %% Description: Gets the db_info as a proplist 86 | %%-------------------------------------------------------------------- 87 | db_info(DbName) -> 88 | {ok, Db} = open_db(DbName), 89 | couch_db:get_db_info(Db). 90 | 91 | %%-------------------------------------------------------------------- 92 | %% Function: open_doc(DbName, DocId) -> {ok,Doc} | {error,Error} 93 | %% Description: Gets the eJSON form of the Document 94 | %%-------------------------------------------------------------------- 95 | open_doc(DbName, DocId) -> 96 | {ok, Db} = open_db(DbName), 97 | CouchDoc = couch_httpd_db:couch_doc_open(Db, DocId, nil, []), 98 | Doc = couch_doc:to_json_obj(CouchDoc, []), 99 | {ok, Doc}. 100 | 101 | %%-------------------------------------------------------------------- 102 | %% Function: save_doc(DbName, Doc) -> {ok, EJsonInfo} | {error,Error} 103 | %% Description: Saves the doc in the database, returns the id and rev 104 | %%-------------------------------------------------------------------- 105 | save_doc(#db{}=Db, Doc) -> 106 | CouchDoc = ejson_to_couch_doc(Doc), 107 | {ok, Rev} = couch_db:update_doc(Db, CouchDoc, []), 108 | {ok, {[{id, CouchDoc#doc.id}, {rev, couch_doc:rev_to_str(Rev)}]}}; 109 | save_doc(DbName, Docs) -> 110 | {ok, Db} = open_db(DbName), 111 | save_doc(Db, Docs). 112 | 113 | %%-------------------------------------------------------------------- 114 | %% Function: save_bulk(DbName, Docs) -> {ok, EJsonInfo} | {error,Error} 115 | %% Description: Saves the docs the database, returns the ids and revs 116 | %%-------------------------------------------------------------------- 117 | save_bulk(#db{}=Db, Docs) -> 118 | CouchDocs = [ejson_to_couch_doc(EJsonDoc) || EJsonDoc <- Docs], 119 | {ok, Results} = couch_db:update_docs(Db, CouchDocs), 120 | {ok, lists:zipwith(fun couch_httpd_db:update_doc_result_to_json/2, 121 | CouchDocs, Results)}; 122 | save_bulk(DbName, Docs) -> 123 | {ok, Db} = open_db(DbName), 124 | save_bulk(Db, Docs). 125 | 126 | 127 | %%-------------------------------------------------------------------- 128 | %% Function: delete_doc(DbName, DocId) -> {ok, {DocId, RevString}} | {error,Error} 129 | %% Description: Deletes the doc 130 | %%-------------------------------------------------------------------- 131 | delete_doc(DbName, {DocProps}) -> 132 | save_doc(DbName, {[{<<"_deleted">>, true}|DocProps]}). 133 | 134 | 135 | %%-------------------------------------------------------------------- 136 | %% Function: start_attachment(DbName, DocId, AName) -> {ok, Pid} | {error,Error} 137 | %% Description: Starts a process to use for fetching attachment bytes 138 | %%-------------------------------------------------------------------- 139 | start_attachment(DbName, DocId, AName) -> 140 | Pid = spawn_link(fun() -> 141 | attachment_streamer(DbName, DocId, AName) 142 | end), 143 | {ok, Pid}. 144 | 145 | %%-------------------------------------------------------------------- 146 | %% Function: next_attachment_bytes(Pid) -> {ok, done} | {error,Error} 147 | %% Description: Fetch attachment bytes 148 | %%-------------------------------------------------------------------- 149 | next_attachment_bytes(Pid) -> 150 | Pid ! {next_attachment_bytes, self()}, 151 | receive 152 | {attachment_done, Pid} -> 153 | {ok, done}; 154 | {attachment_bytes, Pid, {Bin, Length}} -> 155 | {Length, _Reason} = {size(Bin), "assert Bin length match"}, 156 | {ok, Bin} 157 | end. 158 | 159 | query_view(DbName, DesignName, ViewName) -> 160 | query_view(DbName, DesignName, ViewName, #view_query_args{}). 161 | 162 | query_view(DbName, DesignName, ViewName, #view_query_args{}=QueryArgs) -> 163 | % provide a default row collector fun 164 | % don't use this on big data it will balloon in memory 165 | RowCollectorFun = fun(Row, Acc) -> 166 | {ok, [Row | Acc]} 167 | end, 168 | query_view(DbName, DesignName, ViewName, RowCollectorFun, QueryArgs); 169 | 170 | query_view(DbName, DesignName, ViewName, ViewFoldFun) -> 171 | query_view(DbName, DesignName, ViewName, ViewFoldFun, #view_query_args{}). 172 | 173 | % special case for special all_docs view 174 | query_view(DbName, undefined, <<"_all_docs">>, _ViewFoldFun, #view_query_args{ 175 | limit = Limit, 176 | skip = SkipCount, 177 | stale = _Stale, 178 | direction = Dir, 179 | group_level = _GroupLevel, 180 | start_key = StartKey, 181 | start_docid = StartDocId, 182 | end_key = EndKey, 183 | end_docid = EndDocId, 184 | inclusive_end = Inclusive 185 | }=QueryArgs) -> 186 | {ok, Db} = open_db(DbName), 187 | {ok, Info} = couch_db:get_db_info(Db), 188 | TotalRowCount = proplists:get_value(doc_count, Info), 189 | StartId = if is_binary(StartKey) -> StartKey; 190 | true -> StartDocId 191 | end, 192 | EndId = if is_binary(EndKey) -> EndKey; 193 | true -> EndDocId 194 | end, 195 | FoldAccInit = {Limit, SkipCount, undefined, []}, 196 | UpdateSeq = couch_db:get_update_seq(Db), 197 | FoldlFun = couch_httpd_view:make_view_fold_fun(nil, QueryArgs, <<"">>, Db, 198 | UpdateSeq, TotalRowCount, #view_fold_helper_funs{ 199 | reduce_count = fun couch_db:enum_docs_reduce_to_count/1, 200 | start_response = fun start_map_view_fold_fun/6, 201 | send_row = make_include_docs_row_fold_fun() 202 | }), 203 | AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) -> 204 | case couch_doc:to_doc_info(FullDocInfo) of 205 | #doc_info{revs=[#rev_info{deleted=false, rev=Rev}|_]} -> 206 | FoldlFun({{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}}, Offset, Acc); 207 | #doc_info{revs=[#rev_info{deleted=true}|_]} -> 208 | {ok, Acc} 209 | end 210 | end, 211 | Res = couch_db:enum_docs(Db, 212 | AdapterFun, FoldAccInit, [{start_key, StartId}, {dir, Dir}, 213 | {if Inclusive -> end_key; true -> end_key_gt end, EndId}]), 214 | Res; 215 | 216 | 217 | query_view(DbName, DesignName, ViewName, ViewFoldFun, #view_query_args{ 218 | limit = Limit, 219 | skip = SkipCount, 220 | stale = Stale, 221 | direction = Dir, 222 | group_level = GroupLevel, 223 | start_key = StartKey, 224 | start_docid = StartDocId 225 | }=QueryArgs) -> 226 | {ok, Db} = open_db(DbName), 227 | % get view reference 228 | DesignId = <<"_design/", DesignName/binary>>, 229 | case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of 230 | {ok, View, _Group} -> 231 | {ok, RowCount} = couch_view:get_row_count(View), 232 | Start = {StartKey, StartDocId}, 233 | UpdateSeq = couch_db:get_update_seq(Db), 234 | FoldlFun = couch_httpd_view:make_view_fold_fun(nil, 235 | QueryArgs, <<"">>, Db, UpdateSeq, RowCount, 236 | #view_fold_helper_funs{ 237 | reduce_count = fun couch_view:reduce_to_count/1, 238 | start_response = fun start_map_view_fold_fun/6, 239 | send_row = make_map_row_fold_fun(ViewFoldFun) 240 | }), 241 | FoldAccInit = {Limit, SkipCount, undefined, []}, 242 | {ok, _, {_Lim, _, _, {Offset, ViewFoldAcc}}} = 243 | couch_view:fold(View, FoldlFun, FoldAccInit, 244 | [{dir, Dir}, {start_key, Start}]), 245 | {ok, {RowCount, Offset, ViewFoldAcc}}; 246 | {not_found, Reason} -> 247 | case couch_view:get_reduce_view(Db, DesignId, ViewName, Stale) of 248 | {ok, View, _Group} -> 249 | UpdateSeq = couch_db:get_update_seq(Db), 250 | {ok, GroupRowsFun, RespFun} = 251 | couch_httpd_view:make_reduce_fold_funs(nil, 252 | GroupLevel, QueryArgs, <<"">>, 253 | UpdateSeq, 254 | #reduce_fold_helper_funs{ 255 | start_response = fun start_reduce_view_fold_fun/4, 256 | send_row = make_reduce_row_fold_fun(ViewFoldFun) 257 | }), 258 | FoldAccInit = {Limit, SkipCount, undefined, []}, 259 | Opts = [{key_group_fun, GroupRowsFun} | 260 | couch_httpd_view:make_key_options(QueryArgs)], 261 | {ok, {_, _, _, AccResult}} = couch_view:fold_reduce(View, 262 | RespFun, 263 | FoldAccInit, 264 | Opts), 265 | 266 | {ok, AccResult}; 267 | _ -> 268 | throw({not_found, Reason}) 269 | end 270 | end. 271 | 272 | %%-------------------------------------------------------------------- 273 | %%% Internal functions 274 | %%-------------------------------------------------------------------- 275 | 276 | ejson_to_couch_doc({DocProps}) -> 277 | Doc = case proplists:get_value(<<"_id">>, DocProps) of 278 | undefined -> 279 | DocId = couch_uuids:new(), 280 | {[{<<"_id">>, DocId}|DocProps]}; 281 | _DocId -> 282 | {DocProps} 283 | end, 284 | couch_doc:from_json_obj(Doc). 285 | 286 | start_map_view_fold_fun(_Req, _Etag, _RowCount, Offset, _Acc, _UpdateSeq) -> 287 | {ok, nil, {Offset, []}}. 288 | 289 | make_map_row_fold_fun(ViewFoldFun) -> 290 | fun(_Resp, _Db, {{Key, DocId}, Value}, _IncludeDocs, {Offset, Acc}) -> 291 | {Go, NewAcc} = ViewFoldFun({{Key, DocId}, Value}, Acc), 292 | {Go, {Offset, NewAcc}} 293 | end. 294 | 295 | make_include_docs_row_fold_fun() -> 296 | fun(_Resp, Db, Doc, IncludeDocs, {Offset, Acc}) -> 297 | D = couch_httpd_view:view_row_obj(Db, Doc, IncludeDocs), 298 | {ok, {Offset, [D | Acc]}} 299 | end. 300 | 301 | start_reduce_view_fold_fun(_Req, _Etag, _Acc0, _UpdateSeq) -> 302 | {ok, nil, []}. 303 | 304 | make_reduce_row_fold_fun(ViewFoldFun) -> 305 | fun(_Resp, {Key, Value}, Acc) -> 306 | {Go, NewAcc} = ViewFoldFun({Key, Value}, Acc), 307 | {Go, NewAcc} 308 | end. 309 | 310 | 311 | attachment_streamer(DbName, DocId, AName) -> 312 | {ok, Db} = open_db(DbName), 313 | case couch_db:open_doc(Db, DocId, []) of 314 | {ok, #doc{atts=Attachments}=_Doc} -> 315 | case [A || A <- Attachments, A#att.name == AName] of 316 | [] -> 317 | throw({not_found, "Document is missing attachment"}); 318 | [Rec] when is_record(Rec, att) -> 319 | Me = self(), 320 | couch_doc:att_foldl_decode(Rec, 321 | fun(Bins, []) -> 322 | BinSegment = list_to_binary(Bins), 323 | receive 324 | {next_attachment_bytes, From} -> 325 | From ! {attachment_bytes, Me, {BinSegment, size(BinSegment)}} 326 | end, 327 | {ok, []} 328 | end, 329 | [] 330 | ), 331 | receive 332 | {next_attachment_bytes, From} -> 333 | From ! {attachment_done, Me} 334 | end 335 | end; 336 | _Else -> 337 | throw({not_found, "Document not found"}) 338 | end. 339 | 340 | 341 | -------------------------------------------------------------------------------- /hovercraft_test.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% File : hovercraft.erl 3 | %%% Author : J Chris Anderson 4 | %%% Description : Erlang CouchDB access. 5 | %%% 6 | %%% Created : 4 Apr 2009 by J Chris Anderson 7 | %%% License : Apache 2.0 8 | %%%------------------------------------------------------------------- 9 | -module(hovercraft_test). 10 | -vsn("0.1.1"). 11 | 12 | %% see README.md for usage information 13 | 14 | -export([all/0, all/1, lightning/0, lightning/1]). 15 | 16 | -include("src/couchdb/couch_db.hrl"). 17 | 18 | -define(ADMIN_USER_CTX, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). 19 | 20 | 21 | %%-------------------------------------------------------------------- 22 | %%% Tests : Public Test API 23 | %%-------------------------------------------------------------------- 24 | 25 | all() -> 26 | all(<<"hovercraft-test">>). 27 | 28 | all(DbName) -> 29 | ?LOG_INFO("Starting tests in ~p", [DbName]), 30 | should_create_db(DbName), 31 | should_link_to_db_server(DbName), 32 | should_get_db_info(DbName), 33 | should_save_and_open_doc(DbName), 34 | should_stream_attachment(DbName), 35 | should_query_views(DbName), 36 | should_error_on_missing_doc(DbName), 37 | should_save_bulk_docs(DbName), 38 | should_save_bulk_and_open_with_db(DbName), 39 | chain(), 40 | ok. 41 | 42 | chain() -> 43 | DbName = <<"chain-test">>, 44 | TargetName = <<"chain-results-test">>, 45 | should_create_db(DbName), 46 | should_create_db(TargetName), 47 | % make ddoc 48 | DDocName = <<"view-test">>, 49 | {ok, {_Resp}} = hovercraft:save_doc(DbName, make_test_ddoc(DDocName)), 50 | % make docs 51 | {ok, _RevInfos} = make_test_docs(DbName, {[{<<"lorem">>, <<"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.">>}]}, 200), 52 | do_chain(DbName, <<"view-test">>, <<"letter-cloud">>, TargetName). 53 | 54 | %% Performance Tests 55 | lightning() -> 56 | lightning(1000). 57 | 58 | lightning(BulkSize) -> 59 | lightning(BulkSize, 100000, <<"hovercraft-lightning">>). 60 | 61 | lightning(BulkSize, NumDocs, DbName) -> 62 | hovercraft:delete_db(DbName), 63 | {ok, created} = hovercraft:create_db(DbName), 64 | {ok, Db} = hovercraft:open_db(DbName), 65 | Doc = {[{<<"foo">>, <<"bar">>}]}, 66 | StartTime = now(), 67 | insert_in_batches(Db, NumDocs, BulkSize, Doc, 0), 68 | Duration = timer:now_diff(now(), StartTime) / 1000000, 69 | io:format("Inserted ~p docs in ~p seconds with batch size of ~p. (~p docs/sec)~n", 70 | [NumDocs, Duration, BulkSize, NumDocs / Duration]). 71 | 72 | 73 | %%-------------------------------------------------------------------- 74 | %%% Tests : Test Cases 75 | %%-------------------------------------------------------------------- 76 | 77 | should_create_db(DbName) -> 78 | hovercraft:delete_db(DbName), 79 | {ok, created} = hovercraft:create_db(DbName), 80 | {ok, _DbInfo} = hovercraft:db_info(DbName). 81 | 82 | should_link_to_db_server(DbName) -> 83 | {ok, DbInfo} = hovercraft:db_info(DbName), 84 | DbName = proplists:get_value(db_name, DbInfo). 85 | 86 | should_get_db_info(DbName) -> 87 | {ok, DbInfo} = hovercraft:db_info(DbName), 88 | DbName = proplists:get_value(db_name, DbInfo), 89 | 0 = proplists:get_value(doc_count, DbInfo), 90 | 0 = proplists:get_value(update_seq, DbInfo). 91 | 92 | should_save_and_open_doc(DbName) -> 93 | Doc = {[{<<"foo">>, <<"bar">>}]}, 94 | {ok, {Resp}} = hovercraft:save_doc(DbName, Doc), 95 | DocId = proplists:get_value(id, Resp), 96 | {ok, {Props}} = hovercraft:open_doc(DbName, DocId), 97 | <<"bar">> = proplists:get_value(<<"foo">>, Props). 98 | 99 | should_error_on_missing_doc(DbName) -> 100 | try hovercraft:open_doc(DbName, <<"not a doc">>) 101 | catch 102 | throw:{not_found,missing} -> 103 | ok 104 | end. 105 | 106 | should_save_bulk_docs(DbName) -> 107 | Docs = [{[{<<"foo">>, <<"bar">>}]} || _Seq <- lists:seq(1, 100)], 108 | {ok, RevInfos} = hovercraft:save_bulk(DbName, Docs), 109 | lists:foldl(fun(Row, _) -> 110 | DocId = proplists:get_value(id, Row), 111 | {ok, _Doc} = hovercraft:open_doc(DbName, DocId) 112 | end, RevInfos, []). 113 | 114 | should_save_bulk_and_open_with_db(DbName) -> 115 | {ok, Db} = hovercraft:open_db(DbName), 116 | Docs = [{[{<<"foo">>, <<"bar">>}]} || _Seq <- lists:seq(1, 100)], 117 | {ok, RevInfos} = hovercraft:save_bulk(Db, Docs), 118 | lists:foldl(fun(Row, _) -> 119 | DocId = proplists:get_value(id, Row), 120 | {ok, _Doc} = hovercraft:open_doc(Db, DocId) 121 | end, RevInfos, []). 122 | 123 | should_stream_attachment(DbName) -> 124 | % setup 125 | AName = <<"test.txt">>, 126 | ABytes = list_to_binary(string:chars($a, 10*1024, "")), 127 | Doc = {[{<<"foo">>, <<"bar">>}, 128 | {<<"_attachments">>, {[{AName, {[ 129 | {<<"content_type">>, <<"text/plain">>}, 130 | {<<"data">>, base64:encode(ABytes)} 131 | ]}}]}} 132 | ]}, 133 | {ok, {Resp}} = hovercraft:save_doc(DbName, Doc), 134 | DocId = proplists:get_value(id, Resp), 135 | % stream attachment 136 | {ok, Pid} = hovercraft:start_attachment(DbName, DocId, AName), 137 | {ok, Attachment} = get_full_attachment(Pid, []), 138 | ABytes = Attachment, 139 | ok. 140 | 141 | should_query_views(DbName) -> 142 | % make ddoc 143 | DDocName = <<"view-test">>, 144 | {ok, {_Resp}} = hovercraft:save_doc(DbName, make_test_ddoc(DDocName)), 145 | % make docs 146 | {ok, _RevInfos} = make_test_docs(DbName, {[{<<"hovercraft">>, <<"views rule">>}]}, 20), 147 | should_query_map_view(DbName, DDocName), 148 | should_query_reduce_view(DbName, DDocName). 149 | 150 | should_query_map_view(DbName, DDocName) -> 151 | % use the default query arguments and row collector function 152 | {ok, {RowCount, Offset, Rows}} = 153 | hovercraft:query_view(DbName, DDocName, <<"basic">>), 154 | % assert rows is the right length 155 | 20 = length(Rows), 156 | RowCount = length(Rows), 157 | 0 = Offset, 158 | % assert we got every row 159 | lists:foldl(fun({{RKey, RDocId}, RValue}, _) -> 160 | 1 = RValue, 161 | {ok, {DocProps}} = hovercraft:open_doc(RDocId), 162 | RKey = proplists:get_value(<<"_rev">>, DocProps) 163 | end, Rows, []). 164 | 165 | should_query_reduce_view(DbName, DDocName) -> 166 | {ok, [Result]} = 167 | hovercraft:query_view(DbName, DDocName, <<"reduce-sum">>), 168 | {null, 20} = Result, 169 | {ok, Results} = 170 | hovercraft:query_view(DbName, DDocName, <<"reduce-sum">>, #view_query_args{ 171 | group_level = exact 172 | }), 173 | [{_,20}] = Results. 174 | 175 | 176 | %%-------------------------------------------------------------------- 177 | %%% Helper Functions 178 | %%-------------------------------------------------------------------- 179 | 180 | get_full_attachment(Pid, Acc) -> 181 | Val = hovercraft:next_attachment_bytes(Pid), 182 | case Val of 183 | {ok, done} -> 184 | {ok, list_to_binary(lists:reverse(Acc))}; 185 | {ok, Bin} -> 186 | get_full_attachment(Pid, [Bin|Acc]) 187 | end. 188 | 189 | make_test_ddoc(DesignName) -> 190 | {[ 191 | {<<"_id">>, <<"_design/", DesignName/binary>>}, 192 | {<<"views">>, {[ 193 | {<<"basic">>, 194 | {[ 195 | {<<"map">>, 196 | <<"function(doc) { if(doc.hovercraft == 'views rule') emit(doc._rev, 1) }">>} 197 | ]} 198 | },{<<"reduce-sum">>, 199 | {[ 200 | {<<"map">>, 201 | <<"function(doc) { if(doc.hovercraft == 'views rule') emit(doc._rev, 1) }">>}, 202 | {<<"reduce">>, 203 | <<"function(ks,vs,co){ return sum(vs)}">>} 204 | ]} 205 | },{<<"letter-cloud">>, 206 | {[ 207 | {<<"map">>, 208 | <<"function(doc){if (doc.lorem) { 209 | for (var i=0; i < doc.lorem.length; i++) { 210 | var c = doc.lorem[i]; 211 | emit(c,1) 212 | }; 213 | }}">>}, 214 | {<<"reduce">>, 215 | <<"_count">>} 216 | ]} 217 | } 218 | ]}} 219 | ]}. 220 | 221 | make_test_docs(DbName, Doc, Count) -> 222 | Docs = [Doc || _Seq <- lists:seq(1, Count)], 223 | hovercraft:save_bulk(DbName, Docs). 224 | 225 | do_chain(SourceDbName, SourceDDocName, SourceView, TargetDbName) -> 226 | {ok, Db} = hovercraft:open_db(TargetDbName), 227 | CopyToDbFun = fun 228 | (Row, Acc) when length(Acc) > 50 -> 229 | hovercraft:save_bulk(Db, [row_to_doc(Row)|Acc]), 230 | {ok, []}; 231 | (Row, Acc) -> 232 | {ok, [row_to_doc(Row)|Acc]} 233 | end, 234 | {ok, Acc1} = hovercraft:query_view(SourceDbName, SourceDDocName, SourceView, CopyToDbFun, #view_query_args{ 235 | group_level = exact 236 | }), 237 | hovercraft:save_bulk(Db, Acc1), 238 | ok. 239 | 240 | row_to_doc({Key, Value}) -> 241 | {[ 242 | {<<"key">>, Key}, 243 | {<<"value">>,Value} 244 | ]}. 245 | 246 | insert_in_batches(Db, NumDocs, BulkSize, Doc, StartId) when NumDocs > 0 -> 247 | LastId = insert_batch(Db, BulkSize, Doc, StartId), 248 | insert_in_batches(Db, NumDocs - BulkSize, BulkSize, Doc, LastId+1); 249 | insert_in_batches(_, _, _, _, _) -> 250 | ok. 251 | 252 | insert_batch(Db, BulkSize, Doc, StartId) -> 253 | {ok, Docs, LastId} = make_docs_list(Doc, BulkSize, [], StartId), 254 | {ok, _RevInfos} = hovercraft:save_bulk(Db, Docs), 255 | LastId. 256 | 257 | make_docs_list({_DocProps}, 0, Docs, Id) -> 258 | {ok, Docs, Id}; 259 | make_docs_list({DocProps}, Size, Docs, Id) -> 260 | ThisDoc = {[{<<"_id">>, make_id_str(Id)}|DocProps]}, 261 | make_docs_list({DocProps}, Size - 1, [ThisDoc|Docs], Id + 1). 262 | 263 | make_id_str(Num) -> 264 | ?l2b(lists:flatten(io_lib:format("~10..0B", [Num]))). 265 | 266 | --------------------------------------------------------------------------------