diff options
author | John Christopher Anderson <jchris@apache.org> | 2009-01-23 00:53:05 +0000 |
---|---|---|
committer | John Christopher Anderson <jchris@apache.org> | 2009-01-23 00:53:05 +0000 |
commit | 0a46c330072a3811d98a5c989d4c6486cff83df2 (patch) | |
tree | c3eaab8bc703fd0b4c375d70efb1eea42d68a1ed | |
parent | 3e12deff5c0f87eefcd3de8dbf93a9724e98258e (diff) |
View list functions can stream views in any format. See list_views test for details.
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@736876 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r-- | etc/couchdb/default.ini.tpl.in | 1 | ||||
-rw-r--r-- | share/server/main.js | 54 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 296 | ||||
-rw-r--r-- | src/couchdb/couch_db.hrl | 15 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_db.erl | 13 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_external.erl | 13 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_show.erl | 168 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_view.erl | 100 | ||||
-rw-r--r-- | src/couchdb/couch_query_servers.erl | 26 |
9 files changed, 516 insertions, 170 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index d16b1e71..a900a575 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -50,6 +50,7 @@ _restart = {couch_httpd_misc_handlers, handle_restart_req} _view = {couch_httpd_view, handle_view_req} _slow_view = {couch_httpd_view, handle_slow_view_req} _show = {couch_httpd_show, handle_doc_show_req} +_list = {couch_httpd_show, handle_view_list_req} ; The external module takes an optional argument allowing you to narrow it to a ; single script. Otherwise the script name is inferred from the first path section diff --git a/share/server/main.js b/share/server/main.js index 38ca326c..d2b9b7e0 100644 --- a/share/server/main.js +++ b/share/server/main.js @@ -332,28 +332,32 @@ while (cmd = eval(readline())) { validateFun(newDoc, oldDoc, userCtx); print("1"); } catch (error) { - print(toJSON(error)); + respond(error); } break; case "show_doc": var funSrc = cmd[1]; var doc = cmd[2]; var req = cmd[3]; - try { - var formFun = compileFunction(funSrc); - var rendered = formFun(doc, req); - print(toJSON(rendered)); - } catch (error) { - // Available error fields: - // message, fileName, lineNumber, stack, name - log("doc show function raised error: "+error.toString()); - log("stacktrace: "+error.stack); - try { - print(toJSON(error)); - } catch (e) { - print({"error":error.toString()}); - } - } + var formFun = compileFunction(funSrc); + runRenderFunction(formFun, [doc, req]); + break; + case "list_begin": + var listFun = funs[0]; + var head = cmd[1]; + var req = cmd[2]; + runRenderFunction(listFun, [head, null, req]); + break; + case "list_row": + var listFun = funs[0]; + var row = cmd[1]; + var req = cmd[2]; + runRenderFunction(listFun, [null, row, req]); + break; + case "list_tail": + var listFun = funs[0]; + var req = cmd[1]; + runRenderFunction(listFun, [null, null, req]); break; default: print(toJSON({error: "query_server_error", @@ -365,6 +369,24 @@ while (cmd = eval(readline())) { } } +function runRenderFunction(renderFun, args) { + try { + var result = renderFun.apply(null, args); + respond(result); + } catch(e) { + log("function raised error: "+e.toString()); + log("stacktrace: "+e.stack); + } +}; + +// prints the object as JSON, and rescues and logs any toJSON() related errors +function respond(obj) { + try { + print(toJSON(obj)); + } catch(e) { + log("Error converting object to JSON: " + e.toString()); + } +} function compileFunction(source) { try { diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js index dbdc27ce..9fea3be7 100644 --- a/share/www/script/couch_tests.js +++ b/share/www/script/couch_tests.js @@ -2301,44 +2301,43 @@ var tests = { var designDoc = { _id:"_design/template", language: "javascript", - show: { - docs: { - "hello" : stringFun(function() { - return { - body : "Hello World" - }; - }), - "just-name" : stringFun(function(doc, req) { - return { - body : "Just " + doc.name - }; - }), - "req-info" : stringFun(function(doc, req) { - return { - json : req - } - }), - "xml-type" : stringFun(function(doc, req) { - return { - "headers" : { - "Content-Type" : "application/xml" - }, - "body" : new XML('<xml><node foo="bar"/></xml>') - } - }), - "no-set-etag" : stringFun(function(doc, req) { + shows: { + "hello" : stringFun(function() { + return { + body : "Hello World" + }; + }), + "just-name" : stringFun(function(doc, req) { + return { + body : "Just " + doc.name + }; + }), + "req-info" : stringFun(function(doc, req) { + return { + json : req + } + }), + "xml-type" : stringFun(function(doc, req) { + return { + "headers" : { + "Content-Type" : "application/xml" + }, + "body" : new XML('<xml><node foo="bar"/></xml>') + } + }), + "no-set-etag" : stringFun(function(doc, req) { + return { + headers : { + "Etag" : "skipped" + }, + "body" : "something" + } + }), + "accept-switch" : stringFun(function(doc, req) { + if (req.headers["Accept"].match(/image/)) { return { - headers : { - "Etag" : "skipped" - }, - "body" : "something" - } - }), - "accept-switch" : stringFun(function(doc, req) { - if (req.headers["Accept"].match(/image/)) { - return { - // a 16x16 px version of the CouchDB logo - "base64" : + // a 16x16 px version of the CouchDB logo + "base64" : ["iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAsV", "BMVEUAAAD////////////////////////5ur3rEBn////////////////wDBL/", "AADuBAe9EB3IEBz/7+//X1/qBQn2AgP/f3/ilpzsDxfpChDtDhXeCA76AQH/v7", @@ -2348,47 +2347,47 @@ var tests = { "zMxw/4OleiJlHeUtv2X6RbNO1Uqj9g0RMCuQO0vBIg4vMFeOpCWIWmDOw82fZx", "vaND1c8OG4vrdOqD8YwgpDYDxRgkSm5rwu0nQVBJuMg++pLXZyr5jnc1BaH4GT", "LvEliY253nA3pVhQqdPt0f/erJkMGMB8xucAAAAASUVORK5CYII="].join(''), - headers : { - "Content-Type" : "image/png", - "Vary" : "Accept" // we set this for proxy caches - } + headers : { + "Content-Type" : "image/png", + "Vary" : "Accept" // we set this for proxy caches + } + }; + } else { + return { + "body" : "accepting text requests", + headers : { + "Content-Type" : "text/html", + "Vary" : "Accept" + } + }; + } + }), + "respondWith" : stringFun(function(doc, req) { + registerType("foo", "application/foo","application/x-foo"); + return respondWith(req, { + html : function() { + return { + body:"Ha ha, you said \"" + doc.word + "\"." }; - } else { + }, + xml : function() { + var xml = new XML('<xml><node/></xml>'); + // Becase Safari can't stand to see that dastardly + // E4X outside of a string. Outside of tests you + // can just use E4X literals. + this.eval('xml.node.@foo = doc.word'); return { - "body" : "accepting text requests", - headers : { - "Content-Type" : "text/html", - "Vary" : "Accept" - } + body: xml }; - } - }), - "respondWith" : stringFun(function(doc, req) { - registerType("foo", "application/foo","application/x-foo"); - return respondWith(req, { - html : function() { - return { - body:"Ha ha, you said \"" + doc.word + "\"." - }; - }, - xml : function() { - var xml = new XML('<xml><node/></xml>'); - // becase Safari can't stand to see that dastardly - // E4X outside of a string. - this.eval('xml.node.@foo = doc.word'); - return { - body: xml - }; - }, - foo : function() { - return { - body: "foofoo" - }; - }, - fallback : "html" - }); - }) - } + }, + foo : function() { + return { + body: "foofoo" + }; + }, + fallback : "html" + }); + }) } }; T(db.save(designDoc).ok); @@ -2418,12 +2417,15 @@ var tests = { T(resp.error == "not_found"); T(resp.reason == "missing"); + // show with missing func + xhr = CouchDB.request("GET", "/test_suite_db/_show/template/missing/"+docid); + T(xhr.status == 404); + // missing design doc xhr = CouchDB.request("GET", "/test_suite_db/_show/missingdoc/just-name/"+docid); T(xhr.status == 404); var resp = JSON.parse(xhr.responseText); T(resp.error == "not_found"); - T(resp.reason == "missing_design_doc"); // query parameters xhr = CouchDB.request("GET", "/test_suite_db/_show/template/req-info/"+docid+"?foo=bar", { @@ -2504,7 +2506,7 @@ var tests = { T(xhr.status == 304); // update design doc function - designDoc.show.docs["just-name"] = (function(doc, req) { + designDoc.shows["just-name"] = (function(doc, req) { return { body : "Just old " + doc.name }; @@ -2553,6 +2555,146 @@ var tests = { T(xhr.responseText.match(/foofoo/)); }, + list_views : function(debug) { + var db = new CouchDB("test_suite_db"); + db.deleteDb(); + db.createDb(); + if (debug) debugger; + + function stringFun(fun) { + var string = fun.toSource ? fun.toSource() : "(" + fun.toString() + ")"; + return string; + } + + var designDoc = { + _id:"_design/lists", + language: "javascript", + views : { + basicView : { + map : stringFun(function(doc) { + emit(doc.integer, doc.string); + }) + }, + withReduce : { + map : stringFun(function(doc) { + emit(doc.integer, doc.string); + }), + reduce : stringFun(function(keys, values, rereduce) { + if (rereduce) { + return sum(values); + } else { + return values.length; + } + }) + } + }, + lists: { + simpleForm: stringFun(function(head, row, req) { + if (row) { + // we ignore headers on rows and tail + return {body : '\n<li>Key: '+row.key+' Value: '+row.value+'</li>'}; + } else if (head) { + // we return an object (like those used by external and show) + // so that we can specify headers + return { + body : '<h1>Total Rows: ' + + head.total_rows + + ' Offset: ' + head.offset + + '</h1><ul>' + }; + } else { + // tail + return {body : '</ul>'}; + } + }), + acceptSwitch: stringFun(function(head, row, req) { + return respondWith(req, { + html : function() { + if (head) { + return {body : "HTML <ul>"}; + } else if (row) { + return {body : '\n<li>Key: ' + +row.key+' Value: '+row.value+'</li>'}; + } else { // tail + return {body : "</ul>"}; + } + }, + xml : function() { + if (head) { + return {body:'<feed xmlns="http://www.w3.org/2005/Atom">' + +'<title>Test XML Feed</title>'}; + } else if (row) { + // Becase Safari can't stand to see that dastardly + // E4X outside of a string. Outside of tests you + // can just use E4X literals. + var entry = new XML('<entry/>'); + entry.id = row.id; + entry.title = row.key; + entry.content = row.value; + return {body:entry}; + } else { + return {body : "</feed>"}; + } + } + }) + }) + } + }; + + T(db.save(designDoc).ok); + + var docs = makeDocs(0, 10); + var saveResult = db.bulkSave(docs); + T(saveResult.ok); + + var view = db.view('lists/basicView'); + T(view.total_rows == 10); + + // standard get + var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView"); + T(xhr.status == 200); + T(/Total Rows/.test(xhr.responseText)); + T(/Key: 1/.test(xhr.responseText)); + + // get with query params + var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView?startkey=3"); + T(xhr.status == 200); + T(/Total Rows/.test(xhr.responseText)); + T(!(/Key: 1/.test(xhr.responseText))); + + // with 0 rows + var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/basicView?startkey=30"); + T(xhr.status == 200); + T(/Total Rows/.test(xhr.responseText)); + T(/Offset: null/.test(xhr.responseText)); + + // when there is a reduce present, but not used + var xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/simpleForm/withReduce?reduce=false"); + T(xhr.status == 200); + T(/Total Rows/.test(xhr.responseText)); + T(/Key: 1/.test(xhr.responseText)); + + // with accept headers for HTML + xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/acceptSwitch/basicView", { + headers: { + "Accept": 'text/html' + } + }); + T(xhr.getResponseHeader("Content-Type") == "text/html"); + T(xhr.responseText.match(/HTML/)); + T(xhr.responseText.match(/Value/)); + + // now with xml + xhr = CouchDB.request("GET", "/test_suite_db/_list/lists/acceptSwitch/basicView", { + headers: { + "Accept": 'application/xml' + } + }); + T(xhr.getResponseHeader("Content-Type") == "application/xml"); + T(xhr.responseText.match(/XML/)); + T(xhr.responseText.match(/entry/)); + }, + compact: function(debug) { var db = new CouchDB("test_suite_db"); db.deleteDb(); diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl index 083468dc..eb449ab7 100644 --- a/src/couchdb/couch_db.hrl +++ b/src/couchdb/couch_db.hrl @@ -143,7 +143,6 @@ end_key = {}, limit = 10000000000, % a huge huge default number. Picked so we don't have % to do different logic for when there is no limit - % limit stale = false, direction = fwd, start_docid = nil, @@ -154,6 +153,20 @@ include_docs = false }). +-record(view_fold_helper_funs, { + reduce_count, + passed_end, + start_response, + send_row +}). + +-record(extern_resp_args, { + code = 200, + data = <<>>, + ctype = "application/json", + headers = [] +}). + -record(group, {type=view, % can also be slow_view sig=nil, diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index c7c9ec15..9ef7387c 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -192,7 +192,9 @@ db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) -> TotalRowCount = proplists:get_value(doc_count, Info), FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, Db, - TotalRowCount, fun couch_db:enum_docs_since_reduce_to_count/1), + TotalRowCount, #view_fold_helper_funs{ + reduce_count = fun couch_db:enum_docs_since_reduce_to_count/1 + }), StartKey2 = case StartKey of nil -> 0; <<>> -> 100000000000; @@ -306,7 +308,10 @@ all_docs_view(Req, Db, Keys) -> case Keys of nil -> FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, Db, - TotalRowCount, fun couch_db:enum_docs_reduce_to_count/1, PassedEndFun), + TotalRowCount, #view_fold_helper_funs{ + reduce_count = fun couch_db:enum_docs_reduce_to_count/1, + passed_end = PassedEndFun + }), AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) -> case couch_doc:to_doc_info(FullDocInfo) of #doc_info{deleted=false, rev=Rev} -> @@ -320,7 +325,9 @@ all_docs_view(Req, Db, Keys) -> couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult}); _ -> FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, Db, - TotalRowCount, fun(Offset) -> Offset end), + TotalRowCount, #view_fold_helper_funs{ + reduce_count = fun(Offset) -> Offset end + }), KeyFoldFun = case Dir of fwd -> fun lists:foldl/3; diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl index 7a682ddd..692044ba 100644 --- a/src/couchdb/couch_httpd_external.erl +++ b/src/couchdb/couch_httpd_external.erl @@ -14,18 +14,12 @@ -export([handle_external_req/2, handle_external_req/3]). -export([send_external_response/2, json_req_obj/2]). +-export([default_or_content_type/2, parse_external_response/1]). -import(couch_httpd,[send_error/4]). -include("couch_db.hrl"). --record(extern_resp_args, { - code = 200, - data = <<>>, - ctype = "application/json", - headers = [] -}). - % handle_external_req/2 % for the old type of config usage: % _external = {couch_httpd_external, handle_external_req} @@ -121,7 +115,10 @@ parse_external_response({Response}) -> {<<"body">>, Value} -> Args#extern_resp_args{data=Value, ctype="text/html"}; {<<"base64">>, Value} -> - Args#extern_resp_args{data=couch_util:decodeBase64(Value), ctype="application/binary"}; + Args#extern_resp_args{ + data=couch_util:decodeBase64(Value), + ctype="application/binary" + }; {<<"headers">>, {Headers}} -> NewHeaders = lists:map(fun({Header, HVal}) -> {binary_to_list(Header), binary_to_list(HVal)} diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl index d4b4997b..47d3fb46 100644 --- a/src/couchdb/couch_httpd_show.erl +++ b/src/couchdb/couch_httpd_show.erl @@ -12,7 +12,7 @@ -module(couch_httpd_show). --export([handle_doc_show_req/2]). +-export([handle_doc_show_req/2, handle_view_list_req/2]). -include("couch_db.hrl"). @@ -22,50 +22,142 @@ start_json_response/2,send_chunk/2,end_json_response/1, start_chunked_response/3, send_error/4]). -handle_doc_show_req(#httpd{method='GET',path_parts=[_, _, DesignName, ShowName, Docid]}=Req, Db) -> +handle_doc_show_req(#httpd{ + method='GET', + path_parts=[_, _, DesignName, ShowName, Docid] + }=Req, Db) -> DesignId = <<"_design/", DesignName/binary>>, - % Anyway we can dry up this error handling? - case (catch couch_httpd_db:couch_doc_open(Db, DesignId, [], [])) of - {not_found, missing} -> - throw({not_found, missing_design_doc}); - {not_found, deleted} -> - throw({not_found, deleted_design_doc}); - DesignDoc -> - #doc{body={Props}} = DesignDoc, - Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), - case proplists:get_value(<<"show">>, Props, nil) of - {DocAndViews} -> - case proplists:get_value(<<"docs">>, DocAndViews, nil) of - nil -> - throw({not_found, missing_show_docs}); - {DocShows} -> - case proplists:get_value(ShowName, DocShows, nil) of - nil -> - throw({not_found, missing_show_doc_function}); - ShowSrc -> - case (catch couch_httpd_db:couch_doc_open( - Db, Docid, [], [])) of - {not_found, missing} -> - throw({not_found, missing}); - {not_found, deleted} -> - throw({not_found, deleted}); - Doc -> - % ok we have everythign we need. let's make it happen. - send_doc_show_response(Lang, ShowSrc, Doc, Req, Db) - end - end - end; - nil -> - throw({not_found, missing_show}) - end - end; + #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, [], []), + Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), + ShowSrc = get_nested_json_value({Props}, [<<"shows">>, ShowName]), + Doc = couch_httpd_db:couch_doc_open(Db, Docid, [], []), + send_doc_show_response(Lang, ShowSrc, Doc, Req, Db); handle_doc_show_req(#httpd{method='GET'}=Req, _Db) -> - send_error(Req, 404, <<"form_error">>, <<"Invalid path.">>); + send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>); handle_doc_show_req(Req, _Db) -> send_method_not_allowed(Req, "GET,HEAD"). +handle_view_list_req(#httpd{method='GET',path_parts=[_, _, DesignName, ListName, ViewName]}=Req, Db) -> + DesignId = <<"_design/", DesignName/binary>>, + #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, [], []), + Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), + ListSrc = get_nested_json_value({Props}, [<<"lists">>, ListName]), + send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db); + +handle_view_list_req(Req, _Db) -> + send_method_not_allowed(Req, "GET,HEAD"). + + +get_nested_json_value({Props}, [Key|Keys]) -> + case proplists:get_value(Key, Props, nil) of + nil -> throw({not_found, <<"missing json key: ", Key/binary>>}); + Value -> get_nested_json_value(Value, Keys) + end; +get_nested_json_value(Value, []) -> + Value; +get_nested_json_value(_NotJSONObj, _) -> + throw({not_found, json_mismatch}). + +send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db) -> + % TODO add etags when we get view etags + #view_query_args{ + stale = Stale, + reduce = Reduce + } = QueryArgs = couch_httpd_view:parse_view_query(Req), + case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of + {ok, View} -> + output_map_list(Req, Lang, ListSrc, View, Db, QueryArgs); + {not_found, _Reason} -> + case couch_view:get_reduce_view(Db, DesignId, ViewName, Stale) of + {ok, ReduceView} -> + case Reduce of + false -> + MapView = couch_view:extract_map_view(ReduceView), + output_map_list(Req, Lang, ListSrc, MapView, Db, QueryArgs); + _ -> + throw({not_implemented, reduce_view_lists}) + end; + {not_found, Reason} -> + throw({not_found, Reason}) + end + end. + +output_map_list(Req, Lang, ListSrc, View, Db, QueryArgs) -> + #view_query_args{ + limit = Limit, + direction = Dir, + skip = SkipCount, + start_key = StartKey, + start_docid = StartDocId + } = QueryArgs, + {ok, RowCount} = couch_view:get_row_count(View), + Start = {StartKey, StartDocId}, + % get the os process here + % pass it into the view fold with closures + {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), + + StartListRespFun = fun(Req2, Code, TotalViewCount, Offset) -> + JsonResp = couch_query_servers:render_list_head(QueryServer, + Req2, Db, TotalViewCount, Offset), + #extern_resp_args{ + code = Code, + data = BeginBody, + ctype = CType, + headers = Headers + } = couch_httpd_external:parse_external_response(JsonResp), + JsonHeaders = couch_httpd_external:default_or_content_type(CType, Headers), + {ok, Resp} = start_chunked_response(Req, Code, JsonHeaders), + {ok, Resp, binary_to_list(BeginBody)} + end, + + SendListRowFun = fun(Resp, Db2, {{Key, DocId}, Value}, + RowFront, _IncludeDocs) -> + JsonResp = couch_query_servers:render_list_row(QueryServer, + Req, Db2, {{Key, DocId}, Value}), + #extern_resp_args{ + data = RowBody + } = couch_httpd_external:parse_external_response(JsonResp), + RowFront2 = case RowFront of + nil -> []; + _ -> RowFront + end, + send_chunk(Resp, RowFront2 ++ binary_to_list(RowBody)) + end, + + FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, Db, RowCount, + #view_fold_helper_funs{ + reduce_count = fun couch_view:reduce_to_count/1, + start_response = StartListRespFun, + send_row = SendListRowFun + }), + FoldAccInit = {Limit, SkipCount, undefined, []}, + FoldResult = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit), + finish_view_list(Req, Db, QueryServer, RowCount, FoldResult, StartListRespFun). + +finish_view_list(Req, Db, QueryServer, TotalRows, + FoldResult, StartListRespFun) -> + case FoldResult of + {ok, {_, _, undefined, _}} -> + {ok, Resp, BeginBody} = StartListRespFun(Req, 200, TotalRows, null), + JsonTail = couch_query_servers:render_list_tail(QueryServer, Req, Db), + #extern_resp_args{ + data = Tail + } = couch_httpd_external:parse_external_response(JsonTail), + send_chunk(Resp, BeginBody ++ Tail), + send_chunk(Resp, []); + {ok, {_, _, Resp, _AccRevRows}} -> + JsonTail = couch_query_servers:render_list_tail(QueryServer, Req, Db), + #extern_resp_args{ + data = Tail + } = couch_httpd_external:parse_external_response(JsonTail), + send_chunk(Resp, Tail), + send_chunk(Resp, []); + Error -> + throw(Error) + end. + send_doc_show_response(Lang, ShowSrc, #doc{revs=[DocRev|_]}=Doc, #httpd{mochi_req=MReq}=Req, Db) -> % make a term with etag-effecting Req components, but not always changing ones. diff --git a/src/couchdb/couch_httpd_view.erl b/src/couchdb/couch_httpd_view.erl index dca1e6e3..06d89a59 100644 --- a/src/couchdb/couch_httpd_view.erl +++ b/src/couchdb/couch_httpd_view.erl @@ -15,7 +15,8 @@ -export([handle_view_req/2,handle_slow_view_req/2]). --export([parse_view_query/1,parse_view_query/2,make_view_fold_fun/5, make_view_fold_fun/6,finish_view_fold/3]). +-export([parse_view_query/1,parse_view_query/2,make_view_fold_fun/5, + finish_view_fold/3, view_row_obj/3]). -import(couch_httpd, [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, @@ -92,8 +93,7 @@ output_map_view(Req, View, Db, QueryArgs, nil) -> } = QueryArgs, {ok, RowCount} = couch_view:get_row_count(View), Start = {StartKey, StartDocId}, - FoldlFun = make_view_fold_fun(Req, QueryArgs, Db, RowCount, - fun couch_view:reduce_to_count/1), + FoldlFun = make_view_fold_fun(Req, QueryArgs, Db, RowCount, #view_fold_helper_funs{reduce_count=fun couch_view:reduce_to_count/1}), FoldAccInit = {Limit, SkipCount, undefined, []}, FoldResult = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit), finish_view_fold(Req, RowCount, FoldResult); @@ -114,7 +114,10 @@ output_map_view(Req, View, Db, QueryArgs, Keys) -> QueryArgs#view_query_args{ start_key = Key, end_key = Key - }, Db, RowCount, fun couch_view:reduce_to_count/1), + }, Db, RowCount, + #view_fold_helper_funs{ + reduce_count = fun couch_view:reduce_to_count/1 + }), couch_view:fold(View, Start, Dir, FoldlFun, FoldAcc) end, {ok, FoldAccInit}, Keys), finish_view_fold(Req, RowCount, FoldResult). @@ -362,28 +365,21 @@ parse_view_query(Req, Keys, IsReduce) -> end end. - make_view_fold_fun(Req, QueryArgs, Db, - TotalViewCount, ReduceCountFun) -> + TotalViewCount, HelperFuns) -> #view_query_args{ end_key = EndKey, end_docid = EndDocId, direction = Dir } = QueryArgs, - PassedEndFun = - case Dir of - fwd -> - fun(ViewKey, ViewId) -> - couch_view:less_json([EndKey, EndDocId], [ViewKey, ViewId]) - end; - rev-> - fun(ViewKey, ViewId) -> - couch_view:less_json([ViewKey, ViewId], [EndKey, EndDocId]) - end - end, - make_view_fold_fun(Req, QueryArgs, Db, TotalViewCount, ReduceCountFun, PassedEndFun). + + #view_fold_helper_funs{ + passed_end = PassedEndFun, + start_response = StartRespFun, + send_row = SendRowFun, + reduce_count = ReduceCountFun + } = apply_default_helper_funs(HelperFuns, {Dir, EndKey, EndDocId}), -make_view_fold_fun(Req, QueryArgs, Db, TotalViewCount, ReduceCountFun, PassedEndFun) -> #view_query_args{ include_docs = IncludeDocs } = QueryArgs, @@ -401,20 +397,72 @@ make_view_fold_fun(Req, QueryArgs, Db, TotalViewCount, ReduceCountFun, PassedEnd {_, _, AccSkip, _} when AccSkip > 0 -> {ok, {AccLimit, AccSkip - 1, Resp, AccRevRows}}; {_, _, _, undefined} -> - {ok, Resp2} = start_json_response(Req, 200), Offset = ReduceCountFun(OffsetReds), - JsonBegin = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n", - [TotalViewCount, Offset]), - JsonObj = view_row_obj(Db, {{Key, DocId}, Value}, IncludeDocs), - send_chunk(Resp2, JsonBegin ++ ?JSON_ENCODE(JsonObj)), + {ok, Resp2, BeginBody} = StartRespFun(Req, 200, + TotalViewCount, Offset), + SendRowFun(Resp2, Db, + {{Key, DocId}, Value}, BeginBody, IncludeDocs), {ok, {AccLimit - 1, 0, Resp2, AccRevRows}}; {_, AccLimit, _, Resp} when (AccLimit > 0) -> - JsonObj = view_row_obj(Db, {{Key, DocId}, Value}, IncludeDocs), - send_chunk(Resp, ",\r\n" ++ ?JSON_ENCODE(JsonObj)), + SendRowFun(Resp, Db, + {{Key, DocId}, Value}, nil, IncludeDocs), {ok, {AccLimit - 1, 0, Resp, AccRevRows}} end end. +apply_default_helper_funs(#view_fold_helper_funs{ + passed_end = PassedEnd, + start_response = StartResp, + send_row = SendRow +}=Helpers, {Dir, EndKey, EndDocId}) -> + PassedEnd2 = case PassedEnd of + undefined -> make_passed_end_fun(Dir, EndKey, EndDocId); + _ -> PassedEnd + end, + + StartResp2 = case StartResp of + undefined -> fun json_view_start_resp/4; + _ -> StartResp + end, + + SendRow2 = case SendRow of + undefined -> fun send_json_view_row/5; + _ -> SendRow + end, + + Helpers#view_fold_helper_funs{ + passed_end = PassedEnd2, + start_response = StartResp2, + send_row = SendRow2 + }. + +make_passed_end_fun(Dir, EndKey, EndDocId) -> + case Dir of + fwd -> + fun(ViewKey, ViewId) -> + couch_view:less_json([EndKey, EndDocId], [ViewKey, ViewId]) + end; + rev-> + fun(ViewKey, ViewId) -> + couch_view:less_json([ViewKey, ViewId], [EndKey, EndDocId]) + end + end. + +json_view_start_resp(Req, Code, TotalViewCount, Offset) -> + {ok, Resp} = couch_httpd:start_json_response(Req, Code), + BeginBody = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n", + [TotalViewCount, Offset]), + {ok, Resp, BeginBody}. + +send_json_view_row(Resp, Db, {{Key, DocId}, Value}, RowFront, IncludeDocs) -> + JsonObj = view_row_obj(Db, {{Key, DocId}, Value}, IncludeDocs), + RowFront2 = case RowFront of + nil -> ",\r\n"; + _ -> RowFront + end, + send_chunk(Resp, RowFront2 ++ ?JSON_ENCODE(JsonObj)). + + view_row_obj(Db, {{Key, DocId}, Value}, IncludeDocs) -> case DocId of error -> diff --git a/src/couchdb/couch_query_servers.erl b/src/couchdb/couch_query_servers.erl index 8d473381..a6bb6e06 100644 --- a/src/couchdb/couch_query_servers.erl +++ b/src/couchdb/couch_query_servers.erl @@ -17,7 +17,8 @@ -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2,code_change/3,stop/0]). -export([start_doc_map/2, map_docs/2, stop_doc_map/1]). --export([reduce/3, rereduce/3,validate_doc_update/5,render_doc_show/5]). +-export([reduce/3, rereduce/3,validate_doc_update/5]). +-export([render_doc_show/5,start_view_list/2,render_list_head/5, render_list_row/4, render_list_tail/3]). % -export([test/0]). -include("couch_db.hrl"). @@ -134,6 +135,29 @@ render_doc_show(Lang, ShowSrc, Doc, Req, Db) -> ok = ret_os_process(Lang, Pid) end. +start_view_list(Lang, ListSrc) -> + Pid = get_os_process(Lang), + true = couch_os_process:prompt(Pid, [<<"add_fun">>, ListSrc]), + {ok, {Lang, Pid}}. + +render_list_head({_Lang, Pid}, Req, Db, TotalRows, Offset) -> + Head = {[{<<"total_rows">>, TotalRows}, {<<"offset">>, Offset}]}, + JsonReq = couch_httpd_external:json_req_obj(Req, Db), + couch_os_process:prompt(Pid, [<<"list_begin">>, Head, JsonReq]). + +render_list_row({_Lang, Pid}, Req, Db, {{Key, DocId}, Value}) -> + JsonRow = couch_httpd_view:view_row_obj(Db, {{Key, DocId}, Value}, false), + JsonReq = couch_httpd_external:json_req_obj(Req, Db), + couch_os_process:prompt(Pid, [<<"list_row">>, JsonRow, JsonReq]). + +render_list_tail({Lang, Pid}, Req, Db) -> + JsonReq = couch_httpd_external:json_req_obj(Req, Db), + JsonResp = couch_os_process:prompt(Pid, [<<"list_tail">>, JsonReq]), + ok = ret_os_process(Lang, Pid), + JsonResp. + + + init([]) -> % read config and register for configuration changes |