diff --git a/arangod/Aql/Query.cpp b/arangod/Aql/Query.cpp index a467c9563e..dcde331cc3 100644 --- a/arangod/Aql/Query.cpp +++ b/arangod/Aql/Query.cpp @@ -1007,7 +1007,11 @@ QueryResult Query::explain () { it->planRegisters(); out.add(it->toJson(parser.ast(), TRI_UNKNOWN_MEM_ZONE, verbosePlans())); } + result.json = out.steal(); + + // cacheability not available here + result.cached = false; } else { // Now plan and all derived plans belong to the optimizer @@ -1017,6 +1021,10 @@ QueryResult Query::explain () { bestPlan->findVarUsage(); bestPlan->planRegisters(); result.json = bestPlan->toJson(parser.ast(), TRI_UNKNOWN_MEM_ZONE, verbosePlans()).steal(); + + // cacheability + result.cached = (_queryString != nullptr && _queryLength > 0 && + ! _isModificationQuery && _warnings.empty() && _ast->root()->isCacheable()); } _trx->commit(); diff --git a/arangod/V8Server/v8-vocbase.cpp b/arangod/V8Server/v8-vocbase.cpp index 2f10607514..792c787e85 100644 --- a/arangod/V8Server/v8-vocbase.cpp +++ b/arangod/V8Server/v8-vocbase.cpp @@ -1235,7 +1235,9 @@ static void JS_ExplainAql (const v8::FunctionCallbackInfo& args) { } else { result->Set(TRI_V8_ASCII_STRING("plan"), TRI_ObjectJson(isolate, queryResult.json)); + result->Set(TRI_V8_ASCII_STRING("cacheable"), v8::Boolean::New(isolate, queryResult.cached)); } + if (queryResult.clusterplan != nullptr) { result->Set(TRI_V8_ASCII_STRING("clusterplans"), TRI_ObjectJson(isolate, queryResult.clusterplan)); } diff --git a/js/actions/api-explain.js b/js/actions/api-explain.js index 4dd9695e2e..9717860321 100644 --- a/js/actions/api-explain.js +++ b/js/actions/api-explain.js @@ -93,6 +93,10 @@ var ERRORS = require("internal").errors; /// The result will also contain an attribute *warnings*, which is an array of /// warnings that occurred during optimization or execution plan creation. Additionally, /// a *stats* attribute is contained in the result with some optimizer statistics. +/// If *allPlans* is set to *false*, the result will contain an attribute *cacheable* +/// that states whether the query results can be cached on the server if the query +/// result cache were used. The *cacheable* attribute is not present when *allPlans* +/// is set to *true*. /// /// Each plan in the result is a JSON object with the following attributes: /// - *nodes*: the array of execution nodes of the plan. The array of available node types @@ -314,7 +318,8 @@ function post_api_explain (req, res) { result = { plan: result.plan, warnings: result.warnings, - stats: result.stats + stats: result.stats, + cacheable: result.cacheable }; } actions.resultOk(req, res, actions.HTTP_OK, result); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/modules/client/@arangodb/arango-statement.js b/js/apps/system/_admin/aardvark/APP/frontend/js/modules/client/@arangodb/arango-statement.js index 05908c361f..f7778ddcd6 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/modules/client/@arangodb/arango-statement.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/modules/client/@arangodb/arango-statement.js @@ -148,7 +148,8 @@ ArangoStatement.prototype.explain = function (options) { return { plan: requestResult.plan, warnings: requestResult.warnings, - stats: requestResult.stats + stats: requestResult.stats, + cacheable: requestResult.cacheable }; } }; diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/modules/common/@arangodb/aql/explainer.js b/js/apps/system/_admin/aardvark/APP/frontend/js/modules/common/@arangodb/aql/explainer.js index f76f6a792e..140b1e5a1c 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/modules/common/@arangodb/aql/explainer.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/modules/common/@arangodb/aql/explainer.js @@ -126,7 +126,16 @@ function wrap (str, width) { /* print query string */ function printQuery (query) { 'use strict'; - stringBuilder.appendLine(section("Query string:")); + // restrict max length of printed query to avoid endless printing for + // very long query strings + var maxLength = 4096; + if (query.length > maxLength) { + stringBuilder.appendLine(section("Query string (truncated):")); + query = query.substr(0, maxLength / 2) + " ... " + query.substr(query.length - maxLength / 2); + } + else { + stringBuilder.appendLine(section("Query string:")); + } stringBuilder.appendLine(" " + value(wrap(query, 100).replace(/\n+/g, "\n ", query))); stringBuilder.appendLine(); } @@ -261,8 +270,7 @@ function printIndexes (indexes) { } } - -/* print indexes used */ +/* print traversal info */ function printTraversalDetails (traversals) { 'use strict'; if (traversals.length === 0) { @@ -408,7 +416,8 @@ function processQuery (query, explain) { indexes = [ ], traversalDetails = [], modificationFlags, - isConst = true; + isConst = true, + currentNode = null; var variableName = function (node) { try { @@ -427,8 +436,26 @@ function processQuery (query, explain) { return variable(node.name); }; + var addHint = function () { }; + // uncomment this to show "style" hints + // var addHint = function (dst, currentNode, msg) { + // dst.push({ code: "Hint", message: "Node #" + currentNode + ": " + msg }); + // }; + var buildExpression = function (node) { isConst = isConst && ([ "value", "object", "object element", "array" ].indexOf(node.type) !== -1); + + if (node.type !== "attribute access" && + node.hasOwnProperty("subNodes")) { + for (var i = 0; i < node.subNodes.length; ++i) { + if (node.subNodes[i].type === "reference" && + collectionVariables.hasOwnProperty(node.subNodes[i].id)) { + addHint(explain.warnings, currentNode, "reference to collection document variable '" + + node.subNodes[i].name + "' used in potentially non-working way"); + break; + } + } + } switch (node.type) { case "reference": @@ -453,6 +480,7 @@ function processQuery (query, explain) { } return variableName(node); case "collection": + addHint(explain.warnings, currentNode, "using all documents from collection '" + node.name + "' in expression"); return collection(node.name) + " " + annotation("/* all collection documents */"); case "value": return value(JSON.stringify(node.value)); @@ -487,6 +515,21 @@ function processQuery (query, explain) { case "array limit": return buildExpression(node.subNodes[0]) + ", " + buildExpression(node.subNodes[1]); case "attribute access": + if (node.subNodes[0].type === "reference" && + collectionVariables.hasOwnProperty(node.subNodes[0].id)) { + // top-level attribute access + var collectionName = collectionVariables[node.subNodes[0].id], + collectionObject = db._collection(collectionName); + if (collectionObject !== null) { + var isEdgeCollection = (collectionObject.type() === 3), + isSystem = (node.name[0] === '_'); + + if ((isSystem && [ "_key", "_id", "_rev"].concat(isEdgeCollection ? [ "_from", "_to" ] : [ ]).indexOf(node.name) === -1) || + (! isSystem && isEdgeCollection && [ "from", "to" ].indexOf(node.name) !== -1)) { + addHint(explain.warnings, currentNode, "reference to potentially non-existing attribute '" + node.name + "'"); + } + } + } return buildExpression(node.subNodes[0]) + "." + attribute(node.name); case "indexed access": return buildExpression(node.subNodes[0]) + "[" + buildExpression(node.subNodes[1]) + "]"; @@ -810,6 +853,7 @@ function processQuery (query, explain) { var preHandle = function (node) { usedVariables = { }; + currentNode = node.id; isConst = true; if (node.type === "SubqueryNode") { subqueries.push(level); diff --git a/js/client/modules/@arangodb/arango-statement.js b/js/client/modules/@arangodb/arango-statement.js index 03e8b10bb0..e1fc4ac361 100644 --- a/js/client/modules/@arangodb/arango-statement.js +++ b/js/client/modules/@arangodb/arango-statement.js @@ -147,7 +147,8 @@ ArangoStatement.prototype.explain = function (options) { return { plan: requestResult.plan, warnings: requestResult.warnings, - stats: requestResult.stats + stats: requestResult.stats, + cacheable: requestResult.cacheable }; } }; diff --git a/js/common/modules/@arangodb/aql/explainer.js b/js/common/modules/@arangodb/aql/explainer.js index 40615e2248..f0c669906e 100644 --- a/js/common/modules/@arangodb/aql/explainer.js +++ b/js/common/modules/@arangodb/aql/explainer.js @@ -125,7 +125,16 @@ function wrap (str, width) { /* print query string */ function printQuery (query) { 'use strict'; - stringBuilder.appendLine(section("Query string:")); + // restrict max length of printed query to avoid endless printing for + // very long query strings + var maxLength = 4096; + if (query.length > maxLength) { + stringBuilder.appendLine(section("Query string (truncated):")); + query = query.substr(0, maxLength / 2) + " ... " + query.substr(query.length - maxLength / 2); + } + else { + stringBuilder.appendLine(section("Query string:")); + } stringBuilder.appendLine(" " + value(wrap(query, 100).replace(/\n+/g, "\n ", query))); stringBuilder.appendLine(); } @@ -260,8 +269,7 @@ function printIndexes (indexes) { } } - -/* print indexes used */ +/* print traversal info */ function printTraversalDetails (traversals) { 'use strict'; if (traversals.length === 0) { @@ -407,7 +415,8 @@ function processQuery (query, explain) { indexes = [ ], traversalDetails = [], modificationFlags, - isConst = true; + isConst = true, + currentNode = null; var variableName = function (node) { try { @@ -426,8 +435,26 @@ function processQuery (query, explain) { return variable(node.name); }; + var addHint = function () { }; + // uncomment this to show "style" hints + // var addHint = function (dst, currentNode, msg) { + // dst.push({ code: "Hint", message: "Node #" + currentNode + ": " + msg }); + // }; + var buildExpression = function (node) { isConst = isConst && ([ "value", "object", "object element", "array" ].indexOf(node.type) !== -1); + + if (node.type !== "attribute access" && + node.hasOwnProperty("subNodes")) { + for (var i = 0; i < node.subNodes.length; ++i) { + if (node.subNodes[i].type === "reference" && + collectionVariables.hasOwnProperty(node.subNodes[i].id)) { + addHint(explain.warnings, currentNode, "reference to collection document variable '" + + node.subNodes[i].name + "' used in potentially non-working way"); + break; + } + } + } switch (node.type) { case "reference": @@ -452,6 +479,7 @@ function processQuery (query, explain) { } return variableName(node); case "collection": + addHint(explain.warnings, currentNode, "using all documents from collection '" + node.name + "' in expression"); return collection(node.name) + " " + annotation("/* all collection documents */"); case "value": return value(JSON.stringify(node.value)); @@ -486,6 +514,21 @@ function processQuery (query, explain) { case "array limit": return buildExpression(node.subNodes[0]) + ", " + buildExpression(node.subNodes[1]); case "attribute access": + if (node.subNodes[0].type === "reference" && + collectionVariables.hasOwnProperty(node.subNodes[0].id)) { + // top-level attribute access + var collectionName = collectionVariables[node.subNodes[0].id], + collectionObject = db._collection(collectionName); + if (collectionObject !== null) { + var isEdgeCollection = (collectionObject.type() === 3), + isSystem = (node.name[0] === '_'); + + if ((isSystem && [ "_key", "_id", "_rev"].concat(isEdgeCollection ? [ "_from", "_to" ] : [ ]).indexOf(node.name) === -1) || + (! isSystem && isEdgeCollection && [ "from", "to" ].indexOf(node.name) !== -1)) { + addHint(explain.warnings, currentNode, "reference to potentially non-existing attribute '" + node.name + "'"); + } + } + } return buildExpression(node.subNodes[0]) + "." + attribute(node.name); case "indexed access": return buildExpression(node.subNodes[0]) + "[" + buildExpression(node.subNodes[1]) + "]"; @@ -809,6 +852,7 @@ function processQuery (query, explain) { var preHandle = function (node) { usedVariables = { }; + currentNode = node.id; isConst = true; if (node.type === "SubqueryNode") { subqueries.push(level); diff --git a/js/common/tests/shell-statement.js b/js/common/tests/shell-statement.js index 73f8917bad..2eb41b7755 100644 --- a/js/common/tests/shell-statement.js +++ b/js/common/tests/shell-statement.js @@ -252,6 +252,8 @@ function StatementSuite () { assertTrue(plan.hasOwnProperty("collections")); assertEqual([ ], plan.collections); assertTrue(plan.hasOwnProperty("variables")); + assertTrue(result.hasOwnProperty("cacheable")); + assertTrue(result.cacheable); }, //////////////////////////////////////////////////////////////////////////////// @@ -275,6 +277,7 @@ function StatementSuite () { assertTrue(plan.hasOwnProperty("collections")); assertEqual([ ], plan.collections); assertTrue(plan.hasOwnProperty("variables")); + assertFalse(result.hasOwnProperty("cacheable")); }, //////////////////////////////////////////////////////////////////////////////// @@ -298,6 +301,7 @@ function StatementSuite () { assertTrue(plan.hasOwnProperty("collections")); assertEqual([ ], plan.collections); assertTrue(plan.hasOwnProperty("variables")); + assertFalse(result.hasOwnProperty("cacheable")); }, //////////////////////////////////////////////////////////////////////////////// @@ -370,6 +374,8 @@ function StatementSuite () { assertTrue(plan.hasOwnProperty("collections")); assertEqual([ ], plan.collections); assertTrue(plan.hasOwnProperty("variables")); + assertTrue(result.hasOwnProperty("cacheable")); + assertTrue(result.cacheable); }, @@ -393,6 +399,24 @@ function StatementSuite () { assertTrue(plan.hasOwnProperty("collections")); assertEqual([ ], plan.collections); assertTrue(plan.hasOwnProperty("variables")); + assertTrue(result.hasOwnProperty("cacheable")); + assertFalse(result.cacheable); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test non cacheable +//////////////////////////////////////////////////////////////////////////////// + + testExplainNoncacheable : function () { + var st = db._createStatement({ query : "RETURN RAND()" }); + var result = st.explain(); + + assertEqual(0, result.warnings.length); + assertTrue(result.hasOwnProperty("plan")); + assertFalse(result.hasOwnProperty("plans")); + + assertTrue(result.hasOwnProperty("cacheable")); + assertFalse(result.cacheable); }, ////////////////////////////////////////////////////////////////////////////////