From ea3b1153e52ac1513da4d634eedefb05c261039c Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Tue, 22 Dec 2009 18:03:44 +0000 Subject: move query server to a design-doc based protocol, closes COUCHDB-589 git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@893249 13f79535-47bb-0310-9956-ffa450edef68 --- etc/couchdb/default.ini.tpl.in | 1 - share/server/filter.js | 19 +- share/server/loop.js | 145 +++++--- share/server/render.js | 558 +++++++++++++++--------------- share/server/state.js | 36 +- share/server/util.js | 91 ++--- share/server/validate.js | 5 +- share/server/views.js | 104 +++--- share/www/script/test/changes.js | 23 +- share/www/script/test/design_docs.js | 25 ++ share/www/script/test/list_views.js | 55 +-- share/www/script/test/show_documents.js | 25 +- share/www/script/test/update_documents.js | 33 +- src/couchdb/couch_doc.erl | 8 +- src/couchdb/couch_httpd.erl | 10 +- src/couchdb/couch_httpd_db.erl | 52 +-- src/couchdb/couch_httpd_external.erl | 7 +- src/couchdb/couch_httpd_show.erl | 474 +++++++++++-------------- src/couchdb/couch_httpd_view.erl | 87 ++--- src/couchdb/couch_native_process.erl | 243 +++++++------ src/couchdb/couch_os_process.erl | 25 +- src/couchdb/couch_query_servers.erl | 257 ++++++++------ test/view_server/query_server_spec.rb | 426 +++++++++++++++-------- test/view_server/run_native_process.es | 14 +- 24 files changed, 1479 insertions(+), 1244 deletions(-) diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in index 422292ff..71656d26 100644 --- a/etc/couchdb/default.ini.tpl.in +++ b/etc/couchdb/default.ini.tpl.in @@ -76,7 +76,6 @@ _user = {couch_httpd_auth, handle_user_req} _view_cleanup = {couch_httpd_db, handle_view_cleanup_req} _compact = {couch_httpd_db, handle_compact_req} _design = {couch_httpd_db, handle_design_req} -_view = {couch_httpd_view, handle_db_view_req} _temp_view = {couch_httpd_view, handle_temp_view_req} _changes = {couch_httpd_db, handle_changes_req} diff --git a/share/server/filter.js b/share/server/filter.js index a683146a..8ba77e64 100644 --- a/share/server/filter.js +++ b/share/server/filter.js @@ -11,17 +11,14 @@ // the License. var Filter = { - filter : function(funSrc, docs, req, userCtx) { - var filterFun = compileFunction(funSrc); - + filter : function(fun, ddoc, args) { var results = []; - try { - for (var i=0; i < docs.length; i++) { - results.push((filterFun(docs[i], req, userCtx) && true) || false); - }; - respond([true, results]); - } catch (error) { - respond(error); - } + var docs = args[0]; + var req = args[1]; + var userCtx = args[2]; + for (var i=0; i < docs.length; i++) { + results.push((fun.apply(ddoc, [docs[i], req, userCtx]) && true) || false); + }; + respond([true, results]); } }; diff --git a/share/server/loop.js b/share/server/loop.js index 33e87c98..84e35dc5 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -12,21 +12,21 @@ var sandbox = null; -var init_sandbox = function() { +function init_sandbox() { try { // if possible, use evalcx (not always available) sandbox = evalcx(''); - sandbox.emit = emit; - sandbox.sum = sum; + sandbox.emit = Views.emit; + sandbox.sum = Views.sum; sandbox.log = log; - sandbox.toJSON = toJSON; - sandbox.provides = provides; - sandbox.registerType = registerType; - sandbox.start = start; - sandbox.send = send; - sandbox.getRow = getRow; + sandbox.toJSON = Couch.toJSON; + sandbox.provides = Mime.provides; + sandbox.registerType = Mime.registerType; + sandbox.start = Render.start; + sandbox.send = Render.send; + sandbox.getRow = Render.getRow; } catch (e) { - log(toJSON(e)); + log(e.toSource()); } }; init_sandbox(); @@ -36,37 +36,104 @@ init_sandbox(); // // Responses are json values followed by a new line ("\n") -var line, cmd, cmdkey; +var DDoc = (function() { + var ddoc_dispatch = { + "lists" : Render.list, + "shows" : Render.show, + "filters" : Filter.filter, + "updates" : Render.update, + "validate_doc_update" : Validate.validate + }; + var ddocs = {}; + return { + ddoc : function() { + var args = []; + for (var i=0; i < arguments.length; i++) { + args.push(arguments[i]); + }; + var ddocId = args.shift(); + if (ddocId == "new") { + // get the real ddocId. + ddocId = args.shift(); + // store the ddoc, functions are lazily compiled. + ddocs[ddocId] = args.shift(); + print("true"); + } else { + // Couch makes sure we know this ddoc already. + var ddoc = ddocs[ddocId]; + if (!ddoc) throw(["fatal", "query_protocol_error", "uncached design doc: "+ddocId]); + var funPath = args.shift(); + var cmd = funPath[0]; + // the first member of the fun path determines the type of operation + var funArgs = args.shift(); + if (ddoc_dispatch[cmd]) { + // get the function, call the command with it + var point = ddoc; + for (var i=0; i < funPath.length; i++) { + if (i+1 == funPath.length) { + fun = point[funPath[i]] + if (typeof fun != "function") { + fun = Couch.compileFunction(fun); + // cache the compiled fun on the ddoc + point[funPath[i]] = fun + }; + } else { + point = point[funPath[i]] + } + }; -var dispatch = { - "reset" : State.reset, - "add_fun" : State.addFun, - "map_doc" : Views.mapDoc, - "reduce" : Views.reduce, - "rereduce" : Views.rereduce, - "validate" : Validate.validate, - "show" : Render.show, - "update" : Render.update, - "list" : Render.list, - "filter" : Filter.filter -}; + // run the correct responder with the cmd body + ddoc_dispatch[cmd].apply(null, [fun, ddoc, funArgs]); + } else { + // unknown command, quit and hope the restarted version is better + throw(["fatal", "unknown_command", "unknown ddoc command '" + cmd + "'"]); + } + } + } + }; +})(); -while (line = eval(readline())) { - cmd = eval(line); - line_length = line.length; - try { - cmdkey = cmd.shift(); - if (dispatch[cmdkey]) { - // run the correct responder with the cmd body - dispatch[cmdkey].apply(this, cmd); +var Loop = function() { + var line, cmd, cmdkey, dispatch = { + "ddoc" : DDoc.ddoc, + // "view" : Views.handler, + "reset" : State.reset, + "add_fun" : State.addFun, + "map_doc" : Views.mapDoc, + "reduce" : Views.reduce, + "rereduce" : Views.rereduce + }; + function handleError(e) { + var type = e[0]; + if (type == "fatal") { + e[0] = "error"; // we tell the client it was a fatal error by dying + respond(e); + quit(-1); + } else if (type == "error") { + respond(e); + } else if (e.error && e.reason) { + // compatibility with old error format + respond(["error", e.error, e.reason]); } else { - // unknown command, quit and hope the restarted version is better - respond({ - error: "query_server_error", - reason: "unknown command '" + cmdkey + "'"}); - quit(); + respond(["error","unnamed_error",e.toSource()]); } - } catch(e) { - respond(e); - } + }; + while (line = readline()) { + cmd = eval('('+line+')'); + State.line_length = line.length; + try { + cmdkey = cmd.shift(); + if (dispatch[cmdkey]) { + // run the correct responder with the cmd body + dispatch[cmdkey].apply(null, cmd); + } else { + // unknown command, quit and hope the restarted version is better + throw(["fatal", "unknown_command", "unknown command '" + cmdkey + "'"]); + } + } catch(e) { + handleError(e); + } + }; }; + +Loop(); diff --git a/share/server/render.js b/share/server/render.js index f147af89..e19f31c4 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -11,152 +11,115 @@ // the License. -// registerType(name, mime-type, mime-type, ...) -// -// Available in query server sandbox. TODO: The list is cleared on reset. -// This registers a particular name with the set of mimetypes it can handle. -// Whoever registers last wins. -// -// Example: -// registerType("html", "text/html; charset=utf-8"); - -mimesByKey = {}; -keysByMime = {}; -registerType = function() { - var mimes = [], key = arguments[0]; - for (var i=1; i < arguments.length; i++) { - mimes.push(arguments[i]); - }; - mimesByKey[key] = mimes; - for (var i=0; i < mimes.length; i++) { - keysByMime[mimes[i]] = key; - }; -}; - -// Some default types -// Ported from Ruby on Rails -// Build list of Mime types for HTTP responses -// http://www.iana.org/assignments/media-types/ -// http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb - -registerType("all", "*/*"); -registerType("text", "text/plain; charset=utf-8", "txt"); -registerType("html", "text/html; charset=utf-8"); -registerType("xhtml", "application/xhtml+xml", "xhtml"); -registerType("xml", "application/xml", "text/xml", "application/x-xml"); -registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); -registerType("css", "text/css"); -registerType("ics", "text/calendar"); -registerType("csv", "text/csv"); -registerType("rss", "application/rss+xml"); -registerType("atom", "application/atom+xml"); -registerType("yaml", "application/x-yaml", "text/yaml"); -// just like Rails -registerType("multipart_form", "multipart/form-data"); -registerType("url_encoded_form", "application/x-www-form-urlencoded"); -// http://www.ietf.org/rfc/rfc4627.txt -registerType("json", "application/json", "text/x-json"); - -// Start chunks -var startResp = {}; -function start(resp) { - startResp = resp || {}; -}; - -function sendStart() { - startResp = applyContentType((startResp || {}), responseContentType); - respond(["start", chunks, startResp]); - chunks = []; - startResp = {}; -} - -function applyContentType(resp, responseContentType) { - resp["headers"] = resp["headers"] || {}; - if (responseContentType) { - resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; - } - return resp; -} - -// Send chunk -var chunks = []; -function send(chunk) { - chunks.push(chunk.toString()); -}; - -function blowChunks(label) { - respond([label||"chunks", chunks]); - chunks = []; -}; - -var gotRow = false, lastRow = false; -function getRow() { - if (lastRow) return null; - if (!gotRow) { - gotRow = true; - sendStart(); - } else { - blowChunks(); - } - var line = readline(); - var json = eval(line); - if (json[0] == "list_end") { - lastRow = true; - return null; - } - if (json[0] != "list_row") { - respond({ - error: "query_server_error", - reason: "not a row '" + json[0] + "'"}); - quit(); - } - return json[1]; -}; - -var mimeFuns = [], providesUsed, responseContentType; -function provides(type, fun) { - providesUsed = true; - mimeFuns.push([type, fun]); -}; - -function runProvides(req) { - var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; - if (req.query && req.query.format) { - bestKey = req.query.format; - responseContentType = mimesByKey[bestKey][0]; - } else if (accept) { - // log("using accept header: "+accept); - mimeFuns.reverse().forEach(function(mimeFun) { - var mimeKey = mimeFun[0]; - if (mimesByKey[mimeKey]) { - supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); - } - }); - responseContentType = Mimeparse.bestMatch(supportedMimes, accept); - bestKey = keysByMime[responseContentType]; - } else { - // just do the first one - bestKey = mimeFuns[0][0]; - responseContentType = mimesByKey[bestKey][0]; +var Mime = (function() { + // registerType(name, mime-type, mime-type, ...) + // + // Available in query server sandbox. TODO: The list is cleared on reset. + // This registers a particular name with the set of mimetypes it can handle. + // Whoever registers last wins. + // + // Example: + // registerType("html", "text/html; charset=utf-8"); + + var mimesByKey = {}; + var keysByMime = {}; + function registerType() { + var mimes = [], key = arguments[0]; + for (var i=1; i < arguments.length; i++) { + mimes.push(arguments[i]); + }; + mimesByKey[key] = mimes; + for (var i=0; i < mimes.length; i++) { + keysByMime[mimes[i]] = key; + }; } + + // Some default types + // Ported from Ruby on Rails + // Build list of Mime types for HTTP responses + // http://www.iana.org/assignments/media-types/ + // http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb + + registerType("all", "*/*"); + registerType("text", "text/plain; charset=utf-8", "txt"); + registerType("html", "text/html; charset=utf-8"); + registerType("xhtml", "application/xhtml+xml", "xhtml"); + registerType("xml", "application/xml", "text/xml", "application/x-xml"); + registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); + registerType("css", "text/css"); + registerType("ics", "text/calendar"); + registerType("csv", "text/csv"); + registerType("rss", "application/rss+xml"); + registerType("atom", "application/atom+xml"); + registerType("yaml", "application/x-yaml", "text/yaml"); + // just like Rails + registerType("multipart_form", "multipart/form-data"); + registerType("url_encoded_form", "application/x-www-form-urlencoded"); + // http://www.ietf.org/rfc/rfc4627.txt + registerType("json", "application/json", "text/x-json"); - if (bestKey) { - for (var i=0; i < mimeFuns.length; i++) { - if (mimeFuns[i][0] == bestKey) { - bestFun = mimeFuns[i][1]; - break; - } + + var mimeFuns = []; + function provides(type, fun) { + Mime.providesUsed = true; + mimeFuns.push([type, fun]); + }; + + function resetProvides() { + // set globals + Mime.providesUsed = false; + mimeFuns = []; + Mime.responseContentType = null; + }; + + function runProvides(req) { + var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; + if (req.query && req.query.format) { + bestKey = req.query.format; + Mime.responseContentType = mimesByKey[bestKey][0]; + } else if (accept) { + // log("using accept header: "+accept); + mimeFuns.reverse().forEach(function(mimeFun) { + var mimeKey = mimeFun[0]; + if (mimesByKey[mimeKey]) { + supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); + } + }); + Mime.responseContentType = Mimeparse.bestMatch(supportedMimes, accept); + bestKey = keysByMime[Mime.responseContentType]; + } else { + // just do the first one + bestKey = mimeFuns[0][0]; + Mime.responseContentType = mimesByKey[bestKey][0]; + } + + if (bestKey) { + for (var i=0; i < mimeFuns.length; i++) { + if (mimeFuns[i][0] == bestKey) { + bestFun = mimeFuns[i][1]; + break; + } + }; }; + + if (bestFun) { + return bestFun(); + } else { + var supportedTypes = mimeFuns.map(function(mf) {return mimesByKey[mf[0]].join(', ') || mf[0]}); + throw(["error","not_acceptable", + "Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')]); + } }; + - if (bestFun) { - // log("responding with: "+bestKey); - return bestFun(); - } else { - var supportedTypes = mimeFuns.map(function(mf) {return mimesByKey[mf[0]].join(', ') || mf[0]}); - throw({error:"not_acceptable", reason:"Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')}); - } -}; + return { + registerType : registerType, + provides : provides, + resetProvides : resetProvides, + runProvides : runProvides + } +})(); + @@ -167,151 +130,202 @@ function runProvides(req) { //// //// -var Render = { - show : function(funSrc, doc, req) { - var showFun = compileFunction(funSrc); - runShow(showFun, doc, req, funSrc); - }, - update : function(funSrc, doc, req) { - var upFun = compileFunction(funSrc); - runUpdate(upFun, doc, req, funSrc); - }, - list : function(head, req) { - runList(funs[0], head, req, funsrc[0]); +var Render = (function() { + var chunks = []; + + + // Start chunks + var startResp = {}; + function start(resp) { + startResp = resp || {}; + }; + + function sendStart() { + startResp = applyContentType((startResp || {}), Mime.responseContentType); + respond(["start", chunks, startResp]); + chunks = []; + startResp = {}; } -}; -function maybeWrapResponse(resp) { - var type = typeof resp; - if ((type == "string") || (type == "xml")) { - return {body:resp}; - } else { + function applyContentType(resp, responseContentType) { + resp["headers"] = resp["headers"] || {}; + if (responseContentType) { + resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; + } return resp; } -}; -function resetProvides() { - // set globals - providesUsed = false; - mimeFuns = []; - responseContentType = null; -}; + function send(chunk) { + chunks.push(chunk.toString()); + }; + + function blowChunks(label) { + respond([label||"chunks", chunks]); + chunks = []; + }; + + var gotRow = false, lastRow = false; + function getRow() { + if (lastRow) return null; + if (!gotRow) { + gotRow = true; + sendStart(); + } else { + blowChunks(); + } + var line = readline(); + var json = eval('('+line+')'); + if (json[0] == "list_end") { + lastRow = true; + return null; + } + if (json[0] != "list_row") { + throw(["fatal", "list_error", "not a row '" + json[0] + "'"]); + } + return json[1]; + }; + + + function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; + } else { + return resp; + } + }; -// from http://javascript.crockford.com/remedial.html -function typeOf(value) { + // from http://javascript.crockford.com/remedial.html + function typeOf(value) { var s = typeof value; if (s === 'object') { - if (value) { - if (value instanceof Array) { - s = 'array'; - } - } else { - s = 'null'; + if (value) { + if (value instanceof Array) { + s = 'array'; } + } else { + s = 'null'; + } } return s; -}; - -function runShow(showFun, doc, req, funSrc) { - try { - resetProvides(); - var resp = showFun.apply(null, [doc, req]); - - if (providesUsed) { - resp = runProvides(req); - resp = applyContentType(maybeWrapResponse(resp), responseContentType); - } + }; - var type = typeOf(resp); - if (type == 'object' || type == 'string') { - respond(["resp", maybeWrapResponse(resp)]); - } else { - renderError("undefined response from show function"); - } - } catch(e) { - respondError(e, funSrc, true); - } -}; - -function runUpdate(renderFun, doc, req, funSrc) { - try { - var result = renderFun.apply(null, [doc, req]); - var doc = result[0]; - var resp = result[1]; - if (resp) { - respond(["up", doc, maybeWrapResponse(resp)]); - } else { - renderError("undefined response from update function"); + function runShow(fun, ddoc, args) { + try { + Mime.resetProvides(); + var resp = fun.apply(ddoc, args); + + if (Mime.providesUsed) { + resp = Mime.runProvides(args[1]); + resp = applyContentType(maybeWrapResponse(resp), Mime.responseContentType); + } + + var type = typeOf(resp); + if (type == 'object' || type == 'string') { + respond(["resp", maybeWrapResponse(resp)]); + } else { + throw(["error", "render_error", "undefined response from show function"]); + } + } catch(e) { + renderError(e, fun.toSource()); } - } catch(e) { - respondError(e, funSrc, true); - } -}; - -function resetList() { - gotRow = false; - lastRow = false; - chunks = []; - startResp = {}; -}; - -function runList(listFun, head, req, funSrc) { - try { - if (listFun.arity > 2) { - throw("the list API has changed for CouchDB 0.10, please upgrade your code"); + }; + + function runUpdate(fun, ddoc, args) { + try { + var verb = args[1].verb; + // for analytics logging applications you might want to remove the next line + if (verb == "GET") throw(["error","method_not_allowed","Update functions do not allow GET"]); + var result = fun.apply(ddoc, args); + var doc = result[0]; + var resp = result[1]; + var type = typeOf(resp); + if (type == 'object' || type == 'string') { + respond(["up", doc, maybeWrapResponse(resp)]); + } else { + throw(["error", "render_error", "undefined response from update function"]); + } + } catch(e) { + renderError(e, fun.toSource()); } - - resetProvides(); - resetList(); - - var tail = listFun.apply(null, [head, req]); - - if (providesUsed) { - tail = runProvides(req); + }; + + function resetList() { + gotRow = false; + lastRow = false; + chunks = []; + startResp = {}; + }; + + function runList(listFun, ddoc, args) { + try { + Mime.resetProvides(); + resetList(); + head = args[0] + req = args[1] + var tail = listFun.apply(ddoc, args); + + if (Mime.providesUsed) { + tail = Mime.runProvides(req); + } + if (!gotRow) getRow(); + if (typeof tail != "undefined") { + chunks.push(tail); + } + blowChunks("end"); + } catch(e) { + renderError(e, listFun.toSource()); } - - if (!gotRow) { - getRow(); + }; + + function renderError(e, funSrc) { + if (e.error && e.reason || e[0] == "error" || e[0] == "fatal") { + throw(e); + } else { + var logMessage = "function raised error: "+e.toSource()+" \nstacktrace: "+e.stack; + log(logMessage); + throw(["error", "render_error", logMessage]); } - if (typeof tail != "undefined") { - chunks.push(tail); + }; + + function escapeHTML(string) { + return string && string.replace(/&/g, "&") + .replace(//g, ">"); + }; + + + return { + start : start, + send : send, + getRow : getRow, + show : function(fun, ddoc, args) { + // var showFun = Couch.compileFunction(funSrc); + runShow(fun, ddoc, args); + }, + update : function(fun, ddoc, args) { + // var upFun = Couch.compileFunction(funSrc); + runUpdate(fun, ddoc, args); + }, + list : function(fun, ddoc, args) { + runList(fun, ddoc, args); } - blowChunks("end"); - } catch(e) { - respondError(e, funSrc, false); - } -}; - -function renderError(m) { - respond({error : "render_error", reason : m}); -} - -function respondError(e, funSrc, htmlErrors) { - if (e.error && e.reason) { - respond(e); - } else { - var logMessage = "function raised error: "+e.toString(); - log(logMessage); - log("stacktrace: "+e.stack); - var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; - renderError(errorMessage); - } -} - -function escapeHTML(string) { - return string.replace(/&/g, "&") - .replace(//g, ">"); -} - -function htmlRenderError(e, funSrc) { - var msg = ["

Render Error

", - "

JavaScript function raised error: ", - e.toString(), - "

Stacktrace:

",
-    escapeHTML(e.stack),
-    "

Function source:

",
-    escapeHTML(funSrc),
-    "
"].join(''); - return {body:msg}; -}; + }; +})(); + +// send = Render.send; +// getRow = Render.getRow; +// start = Render.start; + +// unused. this will be handled in the Erlang side of things. +// function htmlRenderError(e, funSrc) { +// var msg = ["

Render Error

", +// "

JavaScript function raised error: ", +// e.toString(), +// "

Stacktrace:

",
+//     escapeHTML(e.stack),
+//     "

Function source:

",
+//     escapeHTML(funSrc),
+//     "
"].join(''); +// return {body:msg}; +// }; diff --git a/share/server/state.js b/share/server/state.js index b9bd87aa..9af9e475 100644 --- a/share/server/state.js +++ b/share/server/state.js @@ -10,26 +10,18 @@ // License for the specific language governing permissions and limitations under // the License. -// globals used by other modules and functions -var funs = []; // holds functions used for computation -var funsrc = []; // holds function source for debug info -var query_config = {}; -var State = (function() { - return { - reset : function(config) { - // clear the globals and run gc - funs = []; - funsrc = []; - query_config = config; - init_sandbox(); - gc(); - print("true"); // indicates success - }, - addFun : function(newFun) { - // Compile to a function and add it to funs array - funsrc.push(newFun); - funs.push(compileFunction(newFun)); - print("true"); - } +var State = { + reset : function(config) { + // clear the globals and run gc + State.funs = []; + State.query_config = config || {}; + init_sandbox(); + gc(); + print("true"); // indicates success + }, + addFun : function(newFun) { + // Compile to a function and add it to funs array + State.funs.push(Couch.compileFunction(newFun)); + print("true"); } -})(); +} diff --git a/share/server/util.js b/share/server/util.js index 1f69bf16..bd4abc1d 100644 --- a/share/server/util.js +++ b/share/server/util.js @@ -10,13 +10,50 @@ // License for the specific language governing permissions and limitations under // the License. -toJSON.subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', +var Couch = { + // moving this away from global so we can move to json2.js later + toJSON : function (val) { + if (typeof(val) == "undefined") { + throw "Cannot encode 'undefined' value as JSON"; + } + if (typeof(val) == "xml") { // E4X support + val = val.toXMLString(); + } + if (val === null) { return "null"; } + return (Couch.toJSON.dispatcher[val.constructor.name])(val); + }, + compileFunction : function(source) { + if (!source) throw(["error","not_found","missing function"]); + try { + var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); + } catch (err) { + throw(["error", "compilation_error", err.toSource() + " (" + source + ")"]); + }; + if (typeof(functionObject) == "function") { + return functionObject; + } else { + throw(["error","compilation_error", + "Expression does not eval to a function. (" + source.toSource() + ")"]); + }; + }, + recursivelySeal : function(obj) { + // seal() is broken in current Spidermonkey + seal(obj); + for (var propname in obj) { + if (typeof doc[propname] == "object") { + recursivelySeal(doc[propname]); + } + } + } +} + +Couch.toJSON.subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'}; -toJSON.dispatcher = { +Couch.toJSON.dispatcher = { "Array": function(v) { var buf = []; for (var i = 0; i < v.length; i++) { - buf.push(toJSON(v[i])); + buf.push(Couch.toJSON(v[i])); } return "[" + buf.join(",") + "]"; }, @@ -42,14 +79,14 @@ toJSON.dispatcher = { if (!v.hasOwnProperty(k) || typeof(k) !== "string" || v[k] === undefined) { continue; } - buf.push(toJSON(k) + ": " + toJSON(v[k])); + buf.push(Couch.toJSON(k) + ": " + Couch.toJSON(v[k])); } return "{" + buf.join(",") + "}"; }, "String": function(v) { if (/["\\\x00-\x1f]/.test(v)) { v = v.replace(/([\x00-\x1f\\"])/g, function(a, b) { - var c = toJSON.subs[b]; + var c = Couch.toJSON.subs[b]; if (c) return c; c = b.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); @@ -59,56 +96,22 @@ toJSON.dispatcher = { } }; -function toJSON(val) { - if (typeof(val) == "undefined") { - throw "Cannot encode 'undefined' value as JSON"; - } - if (typeof(val) == "xml") { // E4X support - val = val.toXMLString(); - } - if (val === null) { return "null"; } - return (toJSON.dispatcher[val.constructor.name])(val); -} - -function compileFunction(source) { - try { - var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); - } catch (err) { - throw {error: "compilation_error", - reason: err.toString() + " (" + source + ")"}; - } - if (typeof(functionObject) == "function") { - return functionObject; - } else { - throw {error: "compilation_error", - reason: "expression does not eval to a function. (" + source + ")"}; - } -} - -function recursivelySeal(obj) { - seal(obj); - for (var propname in obj) { - if (typeof doc[propname] == "object") { - recursivelySeal(doc[propname]); - } - } -} - // prints the object as JSON, and rescues and logs any toJSON() related errors function respond(obj) { try { - print(toJSON(obj)); + print(Couch.toJSON(obj)); } catch(e) { log("Error converting object to JSON: " + e.toString()); + log("error on obj: "+ obj.toSource()); } }; -log = function(message) { - // return; +function log(message) { + // return; // idea: query_server_config option for log level if (typeof message == "undefined") { message = "Error: attempting to log message of 'undefined'."; } else if (typeof message != "string") { - message = toJSON(message); + message = Couch.toJSON(message); } respond(["log", message]); }; diff --git a/share/server/validate.js b/share/server/validate.js index 5e5e5f9f..76a14129 100644 --- a/share/server/validate.js +++ b/share/server/validate.js @@ -11,10 +11,9 @@ // the License. var Validate = { - validate : function(funSrc, newDoc, oldDoc, userCtx) { - var validateFun = compileFunction(funSrc); + validate : function(fun, ddoc, args) { try { - validateFun(newDoc, oldDoc, userCtx); + fun.apply(ddoc, args); print("1"); } catch (error) { respond(error); diff --git a/share/server/views.js b/share/server/views.js index 1f12ad2b..ffe63377 100644 --- a/share/server/views.js +++ b/share/server/views.js @@ -10,58 +10,76 @@ // License for the specific language governing permissions and limitations under // the License. -// globals used by views -var map_results = []; // holds temporary emitted values during doc map -// view helper functions -emit = function(key, value) { - map_results.push([key, value]); -} - -sum = function(values) { - var rv = 0; - for (var i in values) { - rv += values[i]; - } - return rv; -} var Views = (function() { + var map_results = []; // holds temporary emitted values during doc map + function runReduce(reduceFuns, keys, values, rereduce) { for (var i in reduceFuns) { - reduceFuns[i] = compileFunction(reduceFuns[i]); - } + reduceFuns[i] = Couch.compileFunction(reduceFuns[i]); + }; var reductions = new Array(reduceFuns.length); for(var i = 0; i < reduceFuns.length; i++) { try { reductions[i] = reduceFuns[i](keys, values, rereduce); } catch (err) { - if (err == "fatal_error") { - throw { - error: "reduce_runtime_error", - reason: "function raised fatal exception"}; - } - log("function raised exception (" + err + ")"); + handleViewError(err); + // if the error is not fatal, ignore the results and continue reductions[i] = null; } - } - var reduce_line = toJSON(reductions); + }; + var reduce_line = Couch.toJSON(reductions); var reduce_length = reduce_line.length; - if (query_config && query_config.reduce_limit && - reduce_length > 200 && ((reduce_length * 2) > line.length)) { - var reduce_preview = "Current output: '"+(reduce_line.substring(0,100) + "'... (first 100 of "+reduce_length+' bytes)'); - - throw { - error:"reduce_overflow_error", - reason: "Reduce output must shrink more rapidly: "+reduce_preview+"" - }; + // TODO make reduce_limit config into a number + if (State.query_config && State.query_config.reduce_limit && + reduce_length > 200 && ((reduce_length * 2) > State.line_length)) { + var reduce_preview = "Current output: '"+(reduce_line.substring(0,100) + "'... (first 100 of "+reduce_length+" bytes)"); + throw(["error", + "reduce_overflow_error", + "Reduce output must shrink more rapidly: "+reduce_preview]); } else { print("[true," + reduce_line + "]"); } }; + function handleViewError(err, doc) { + if (err == "fatal_error") { + // Only if it's a "fatal_error" do we exit. What's a fatal error? + // That's for the query to decide. + // + // This will make it possible for queries to completely error out, + // by catching their own local exception and rethrowing a + // fatal_error. But by default if they don't do error handling we + // just eat the exception and carry on. + // + // In this case we abort map processing but don't destroy the + // JavaScript process. If you need to destroy the JavaScript + // process, throw the error form matched by the block below. + throw(["error", "map_runtime_error", "function raised 'fatal_error'"]); + } else if (err[0] == "fatal") { + // Throwing errors of the form ["fatal","error_key","reason"] + // will kill the OS process. This is not normally what you want. + throw(err); + } + var message = "function raised exception " + err.toSource(); + if (doc) message += " with doc._id " + doc._id; + log(message); + }; + return { + // view helper functions + emit : function(key, value) { + map_results.push([key, value]); + }, + sum : function(values) { + var rv = 0; + for (var i in values) { + rv += values[i]; + } + return rv; + }, reduce : function(reduceFuns, kvs) { var keys = new Array(kvs.length); var values = new Array(kvs.length); @@ -101,25 +119,15 @@ var Views = (function() { recursivelySeal(doc); // seal to prevent map functions from changing doc */ var buf = []; - for (var i = 0; i < funs.length; i++) { + for (var i = 0; i < State.funs.length; i++) { map_results = []; try { - funs[i](doc); - buf.push(toJSON(map_results)); + State.funs[i](doc); + buf.push(Couch.toJSON(map_results)); } catch (err) { - if (err == "fatal_error") { - // Only if it's a "fatal_error" do we exit. What's a fatal error? - // That's for the query to decide. - // - // This will make it possible for queries to completely error out, - // by catching their own local exception and rethrowing a - // fatal_error. But by default if they don't do error handling we - // just eat the exception and carry on. - throw { - error: "map_runtime_error", - reason: "function raised fatal exception"}; - } - log("function raised exception (" + err + ") with doc._id " + doc._id); + handleViewError(err, doc); + // If the error is not fatal, we treat the doc as if it + // did not emit anything, by buffering an empty array. buf.push("[]"); } } diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js index 96ddf1a4..0cbf3bd6 100644 --- a/share/www/script/test/changes.js +++ b/share/www/script/test/changes.js @@ -213,12 +213,12 @@ couchTests.changes = function(debug) { xhr = CouchDB.newXhr(); xhr.open("GET", "/test_suite_db/_changes?feed=longpoll&since=7&filter=changes_filter/bop", true); xhr.send(""); - db.save({"bop" : ""}); // empty string is falsy - var id = db.save({"bop" : "bingo"}).id; + db.save({"_id":"falsy", "bop" : ""}); // empty string is falsy + db.save({"_id":"bingo","bop" : "bingo"}); sleep(100); var resp = JSON.parse(xhr.responseText); T(resp.last_seq == 9); - T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == id, "filter the correct update"); + T(resp.results && resp.results.length > 0 && resp.results[0]["id"] == "bingo", "filter the correct update"); // filter with continuous xhr = CouchDB.newXhr(); @@ -226,30 +226,29 @@ couchTests.changes = function(debug) { xhr.send(""); db.save({"_id":"rusty", "bop" : "plankton"}); T(db.ensureFullCommit().ok); - sleep(200); + sleep(300); var lines = xhr.responseText.split("\n"); - T(JSON.parse(lines[1]).id == id); - T(JSON.parse(lines[2]).id == "rusty"); - T(JSON.parse(lines[3]).last_seq == 10); + T(JSON.parse(lines[1]).id == "bingo", lines[1]); + T(JSON.parse(lines[2]).id == "rusty", lines[2]); + T(JSON.parse(lines[3]).last_seq == 10, lines[3]); } - // error conditions // non-existing design doc var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=nothingtosee/bop"); - TEquals(400, req.status, "should return 400 for non existant design doc"); + TEquals(404, req.status, "should return 404 for non existant design doc"); // non-existing filter var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=changes_filter/movealong"); - TEquals(400, req.status, "should return 400 for non existant filter fun"); + TEquals(404, req.status, "should return 404 for non existant filter fun"); // both var req = CouchDB.request("GET", "/test_suite_db/_changes?filter=nothingtosee/movealong"); - TEquals(400, req.status, - "should return 400 for non existant design doc and filter fun"); + TEquals(404, req.status, + "should return 404 for non existant design doc and filter fun"); // changes get all_docs style with deleted docs var doc = {a:1}; diff --git a/share/www/script/test/design_docs.js b/share/www/script/test/design_docs.js index 82c186f8..9318d2bc 100644 --- a/share/www/script/test/design_docs.js +++ b/share/www/script/test/design_docs.js @@ -12,8 +12,11 @@ couchTests.design_docs = function(debug) { var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"}); + var db2 = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"}); db.deleteDb(); db.createDb(); + db2.deleteDb(); + db2.createDb(); if (debug) debugger; run_on_modified_server( @@ -45,10 +48,32 @@ function() { reduce:"function (keys, values) { return sum(values); };"}, huge_src_and_results: {map: "function(doc) { if (doc._id == \"1\") { emit(\"" + makebigstring(16) + "\", null) }}", reduce:"function (keys, values) { return \"" + makebigstring(16) + "\"; };"} + }, + shows: { + simple: "function() {return 'ok'};" } } + var xhr = CouchDB.request("PUT", "/test_suite_db_a/_design/test", {body: JSON.stringify(designDoc)}); + var resp = JSON.parse(xhr.responseText); + + TEquals(resp.rev, db.save(designDoc).rev); + + // test that editing a show fun on the ddoc results in a change in output + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ok"); + + designDoc.shows.simple = "function() {return 'ko'};" T(db.save(designDoc).ok); + var xhr = CouchDB.request("GET", "/test_suite_db/_design/test/_show/simple"); + T(xhr.status == 200); + TEquals(xhr.responseText, "ko"); + + var xhr = CouchDB.request("GET", "/test_suite_db_a/_design/test/_show/simple?cache=buster"); + T(xhr.status == 200); + TEquals("ok", xhr.responseText, 'query server used wrong ddoc'); + // test that we get design doc info back var dinfo = db.designInfo("_design/test"); TEquals("test", dinfo.name); diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index d0400ff9..68dfe71c 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -62,12 +62,7 @@ couchTests.list_views = function(debug) { }), simpleForm: stringFun(function(head, req) { log("simpleForm"); - send('

Total Rows: ' - // + head.total_rows - // + ' Offset: ' + head.offset - + '