/*jshint strict: false, maxlen: 300 */ var db = require("@arangodb").db, internal = require("internal"), systemColors = internal.COLORS, print = internal.print, colors = { }; if (typeof internal.printBrowser === "function") { print = internal.printBrowser; } var stringBuilder = { output: "", appendLine: function(line) { if (!line) { this.output += "\n"; } else { this.output += line + "\n"; } }, getOutput: function() { return this.output; }, clearOutput: function() { this.output = ""; } }; /* set colors for output */ function setColors (useSystemColors) { 'use strict'; [ "COLOR_RESET", "COLOR_CYAN", "COLOR_BLUE", "COLOR_GREEN", "COLOR_MAGENTA", "COLOR_YELLOW", "COLOR_RED", "COLOR_WHITE", "COLOR_BOLD_CYAN", "COLOR_BOLD_BLUE", "COLOR_BOLD_GREEN", "COLOR_BOLD_MAGENTA", "COLOR_BOLD_YELLOW", "COLOR_BOLD_RED", "COLOR_BOLD_WHITE" ].forEach(function(c) { colors[c] = useSystemColors ? systemColors[c] : ""; }); } /* colorizer and output helper functions */ function bracketize (node, v) { 'use strict'; if (node && node.subNodes && node.subNodes.length > 1) { return "(" + v + ")"; } return v; } function attributeUncolored (v) { 'use strict'; return "`" + v + "`"; } function keyword (v) { 'use strict'; return colors.COLOR_CYAN + v + colors.COLOR_RESET; } function annotation (v) { 'use strict'; return colors.COLOR_BLUE + v + colors.COLOR_RESET; } function value (v) { 'use strict'; if (typeof v === 'string' && v.length > 1024) { return colors.COLOR_GREEN + v.substr(0, 1024) + "..." + colors.COLOR_RESET; } return colors.COLOR_GREEN + v + colors.COLOR_RESET; } function variable (v) { 'use strict'; if (v[0] === "#") { return colors.COLOR_MAGENTA + v + colors.COLOR_RESET; } return colors.COLOR_YELLOW + v + colors.COLOR_RESET; } function func (v) { 'use strict'; return colors.COLOR_GREEN + v + colors.COLOR_RESET; } function collection (v) { 'use strict'; return colors.COLOR_RED + v + colors.COLOR_RESET; } function attribute (v) { 'use strict'; return "`" + colors.COLOR_YELLOW + v + colors.COLOR_RESET + "`"; } function header (v) { 'use strict'; return colors.COLOR_MAGENTA + v + colors.COLOR_RESET; } function section (v) { 'use strict'; return colors.COLOR_BOLD_BLUE + v + colors.COLOR_RESET; } function pad (n) { 'use strict'; if (n < 0) { // value seems invalid... n = 0; } return new Array(n).join(" "); } function wrap (str, width) { 'use strict'; var re = ".{1," + width + "}(\\s|$)|\\S+?(\\s|$)"; return str.match(new RegExp(re, "g")).join("\n"); } /* print functions */ /* print query string */ function printQuery (query) { 'use strict'; // 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(); } /* print write query modification flags */ function printModificationFlags (flags) { 'use strict'; if (flags === undefined) { return; } stringBuilder.appendLine(section("Write query options:")); var keys = Object.keys(flags), maxLen = "Option".length; keys.forEach(function(k) { if (k.length > maxLen) { maxLen = k.length; } }); stringBuilder.appendLine(" " + header("Option") + pad(1 + maxLen - "Option".length) + " " + header("Value")); keys.forEach(function(k) { stringBuilder.appendLine(" " + keyword(k) + pad(1 + maxLen - k.length) + " " + value(JSON.stringify(flags[k]))); }); stringBuilder.appendLine(); } /* print optimizer rules */ function printRules (rules) { 'use strict'; stringBuilder.appendLine(section("Optimization rules applied:")); if (rules.length === 0) { stringBuilder.appendLine(" " + value("none")); } else { var maxIdLen = String("Id").length; stringBuilder.appendLine(" " + pad(1 + maxIdLen - String("Id").length) + header("Id") + " " + header("RuleName")); for (var i = 0; i < rules.length; ++i) { stringBuilder.appendLine(" " + pad(1 + maxIdLen - String(i + 1).length) + variable(String(i + 1)) + " " + keyword(rules[i])); } } stringBuilder.appendLine(); } /* print warnings */ function printWarnings (warnings) { 'use strict'; if (! Array.isArray(warnings) || warnings.length === 0) { return; } stringBuilder.appendLine(section("Warnings:")); var maxIdLen = String("Code").length; stringBuilder.appendLine(" " + pad(1 + maxIdLen - String("Code").length) + header("Code") + " " + header("Message")); for (var i = 0; i < warnings.length; ++i) { stringBuilder.appendLine(" " + pad(1 + maxIdLen - String(warnings[i].code).length) + variable(warnings[i].code) + " " + keyword(warnings[i].message)); } stringBuilder.appendLine(); } /* print indexes used */ function printIndexes (indexes) { 'use strict'; stringBuilder.appendLine(section("Indexes used:")); if (indexes.length === 0) { stringBuilder.appendLine(" " + value("none")); } else { var maxIdLen = String("By").length; var maxCollectionLen = String("Collection").length; var maxUniqueLen = String("Unique").length; var maxSparseLen = String("Sparse").length; var maxTypeLen = String("Type").length; var maxSelectivityLen = String("Selectivity").length; var maxFieldsLen = String("Fields").length; indexes.forEach(function(index) { var l = String(index.node).length; if (l > maxIdLen) { maxIdLen = l; } l = index.type.length; if (l > maxTypeLen) { maxTypeLen = l; } l = index.fields.map(attributeUncolored).join(", ").length + "[ ]".length; if (l > maxFieldsLen) { maxFieldsLen = l; } l = index.collection.length; if (l > maxCollectionLen) { maxCollectionLen = l; } }); var line = " " + pad(1 + maxIdLen - String("By").length) + header("By") + " " + header("Type") + pad(1 + maxTypeLen - "Type".length) + " " + header("Collection") + pad(1 + maxCollectionLen - "Collection".length) + " " + header("Unique") + pad(1 + maxUniqueLen - "Unique".length) + " " + header("Sparse") + pad(1 + maxSparseLen - "Sparse".length) + " " + header("Selectivity") + " " + header("Fields") + pad(1 + maxFieldsLen - "Fields".length) + " " + header("Ranges"); stringBuilder.appendLine(line); for (var i = 0; i < indexes.length; ++i) { var uniqueness = (indexes[i].unique ? "true" : "false"); var sparsity = (indexes[i].hasOwnProperty("sparse") ? (indexes[i].sparse ? "true" : "false") : "n/a"); var fields = "[ " + indexes[i].fields.map(attribute).join(", ") + " ]"; var fieldsLen = indexes[i].fields.map(attributeUncolored).join(", ").length + "[ ]".length; var ranges; if (indexes[i].hasOwnProperty("condition")) { ranges = indexes[i].condition; } else { ranges = "[ " + indexes[i].ranges + " ]"; } var selectivity = (indexes[i].hasOwnProperty("selectivityEstimate") ? (indexes[i].selectivityEstimate * 100).toFixed(2) + " %" : "n/a" ); line = " " + pad(1 + maxIdLen - String(indexes[i].node).length) + variable(String(indexes[i].node)) + " " + keyword(indexes[i].type) + pad(1 + maxTypeLen - indexes[i].type.length) + " " + collection(indexes[i].collection) + pad(1 + maxCollectionLen - indexes[i].collection.length) + " " + value(uniqueness) + pad(1 + maxUniqueLen - uniqueness.length) + " " + value(sparsity) + pad(1 + maxSparseLen - sparsity.length) + " " + pad(1 + maxSelectivityLen - selectivity.length) + value(selectivity) + " " + fields + pad(1 + maxFieldsLen - fieldsLen) + " " + ranges; stringBuilder.appendLine(line); } } } /* print traversal info */ function printTraversalDetails (traversals) { 'use strict'; if (traversals.length === 0) { return; } stringBuilder.appendLine(); stringBuilder.appendLine(section("Traversals on graphs:")); var maxIdLen = String("Id").length; var maxMinMaxDepth = String("Depth").length; var maxVertexCollectionNameStrLen = String("Vertex collections").length; var maxEdgeCollectionNameStrLen = String("Edge collections").length; var maxConditionsLen = String("Filter conditions").length; traversals.forEach(function(node) { var l = String(node.id).length; if (l > maxIdLen) { maxIdLen = l; } if (node.minMaxDepthLen > maxMinMaxDepth) { maxMinMaxDepth = node.minMaxDepthLen; } if (node.hasOwnProperty('ConditionStr')) { if (node.ConditionStr.length > maxConditionsLen) { maxConditionsLen = node.ConditionStr.length; } } if (node.hasOwnProperty('vertexCollectionNameStr')) { if (node.vertexCollectionNameStrLen > maxVertexCollectionNameStrLen) { maxVertexCollectionNameStrLen = node.vertexCollectionNameStrLen; } } if (node.hasOwnProperty('edgeCollectionNameStr')) { if (node.edgeCollectionNameStrLen > maxEdgeCollectionNameStrLen) { maxEdgeCollectionNameStrLen = node.edgeCollectionNameStrLen; } } }); var line = " " + pad(1 + maxIdLen - String("Id").length) + header("Id") + " " + header("Depth") + pad(1 + maxMinMaxDepth - String("Depth").length) + " " + header("Vertex collections") + pad(1 + maxVertexCollectionNameStrLen - "Vertex collections".length) + " " + header("Edge collections") + pad(1 + maxEdgeCollectionNameStrLen - "Edge collections".length) + " " + header("Filter conditions"); stringBuilder.appendLine(line); for (var i = 0; i < traversals.length; ++i) { line = " " + pad(1 + maxIdLen - String(traversals[i].id).length) + traversals[i].id + " "; line += traversals[i].minMaxDepth + pad(1 + maxMinMaxDepth - traversals[i].minMaxDepthLen) + " "; if (traversals[i].hasOwnProperty('vertexCollectionNameStr')) { line += traversals[i].vertexCollectionNameStr + pad(1 + maxVertexCollectionNameStrLen - traversals[i].vertexCollectionNameStrLen) + " "; } else { line += pad(1 + maxVertexCollectionNameStrLen) + " "; } if (traversals[i].hasOwnProperty('edgeCollectionNameStr')) { line += traversals[i].edgeCollectionNameStr + pad(1 + maxEdgeCollectionNameStrLen - traversals[i].edgeCollectionNameStrLen) + " "; } else { line += pad(1 + maxEdgeCollectionNameStrLen) + " "; } if (traversals[i].hasOwnProperty('ConditionStr')) { line += traversals[i].ConditionStr; } stringBuilder.appendLine(line); } } /* analzye and print execution plan */ function processQuery (query, explain) { 'use strict'; var nodes = { }, parents = { }, rootNode = null, maxTypeLen = 0, maxSiteLen = 0, maxIdLen = String("Id").length, maxEstimateLen = String("Est.").length, plan = explain.plan; var isOnServer = (typeof ArangoClusterComm === "object"); var cluster; if (isOnServer) { cluster = require("@arangodb/cluster"); } else { cluster = {}; } var recursiveWalk = function (n, level) { n.forEach(function(node) { nodes[node.id] = node; if (level === 0 && node.dependencies.length === 0) { rootNode = node.id; } if (node.type === "SubqueryNode") { recursiveWalk(node.subquery.nodes, level + 1); } node.dependencies.forEach(function(d) { if (! parents.hasOwnProperty(d)) { parents[d] = [ ]; } parents[d].push(node.id); }); if (String(node.id).length > maxIdLen) { maxIdLen = String(node.id).length; } if (String(node.type).length > maxTypeLen) { maxTypeLen = String(node.type).length; } if (String(node.site).length > maxSiteLen) { maxSiteLen = String(node.site).length; } if (String(node.estimatedNrItems).length > maxEstimateLen) { maxEstimateLen = String(node.estimatedNrItems).length; } }); var count = n.length, site = "COOR"; while (count > 0) { --count; var node = n[count]; node.site = site; if (node.type === "RemoteNode") { site = (site === "COOR" ? "DBS" : "COOR"); } } }; recursiveWalk(plan.nodes, 0); var references = { }, collectionVariables = { }, usedVariables = { }, indexes = [ ], traversalDetails = [], modificationFlags, isConst = true, currentNode = null; var variableName = function (node) { try { if (/^[0-9_]/.test(node.name)) { return variable("#" + node.name); } } catch (x) { print(node); throw x; } if (collectionVariables.hasOwnProperty(node.id)) { usedVariables[node.name] = collectionVariables[node.id]; } 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) { var binaryOperator = function (node, name) { var lhs = buildExpression(node.subNodes[0]); var rhs = buildExpression(node.subNodes[1]); if (node.subNodes.length === 3) { // array operator node... prepend "all" | "any" | "none" to node type name = node.subNodes[2].quantifier + " " + name; } if (node.sorted) { return lhs + " " + name + " " + annotation("/* sorted */") + " " + rhs; } return lhs + " " + name + " " + rhs; }; 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": if (references.hasOwnProperty(node.name)) { var ref = references[node.name]; delete references[node.name]; if (Array.isArray(ref)) { var out = buildExpression(ref[1]) + "[" + (new Array(ref[0] + 1).join('*')); if (ref[2].type !== "no-op") { out += " " + keyword("FILTER") + " " + buildExpression(ref[2]); } if (ref[3].type !== "no-op") { out += " " + keyword("LIMIT ") + " " + buildExpression(ref[3]); } if (ref[4].type !== "no-op") { out += " " + keyword("RETURN ") + " " + buildExpression(ref[4]); } out += "]"; return out; } return buildExpression(ref) + "[*]"; } 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)); case "object": if (node.hasOwnProperty("subNodes")) { if (node.subNodes.length > 20) { // print only the first 20 values from the objects return "{ " + node.subNodes.slice(0, 20).map(buildExpression).join(", ") + ", ... }"; } return "{ " + node.subNodes.map(buildExpression).join(", ") + " }"; } return "{ }"; case "object element": return value(JSON.stringify(node.name)) + " : " + buildExpression(node.subNodes[0]); case "calculated object element": return "[ " + buildExpression(node.subNodes[0]) + " ] : " + buildExpression(node.subNodes[1]); case "array": if (node.hasOwnProperty("subNodes")) { if (node.subNodes.length > 20) { // print only the first 20 values from the array return "[ " + node.subNodes.slice(0, 20).map(buildExpression).join(", ") + ", ... ]"; } return "[ " + node.subNodes.map(buildExpression).join(", ") + " ]"; } return "[ ]"; case "unary not": return "! " + buildExpression(node.subNodes[0]); case "unary plus": return "+ " + buildExpression(node.subNodes[0]); case "unary minus": return "- " + buildExpression(node.subNodes[0]); 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]) + "]"; case "range": return buildExpression(node.subNodes[0]) + " .. " + buildExpression(node.subNodes[1]) + " " + annotation("/* range */"); case "expand": case "expansion": if (node.subNodes.length > 2) { // [FILTER ...] references[node.subNodes[0].subNodes[0].name] = [ node.levels, node.subNodes[0].subNodes[1], node.subNodes[2], node.subNodes[3], node.subNodes[4] ]; } else { // [*] references[node.subNodes[0].subNodes[0].name] = node.subNodes[0].subNodes[1]; } return buildExpression(node.subNodes[1]); case "user function call": return func(node.name) + "(" + ((node.subNodes && node.subNodes[0].subNodes) || [ ]).map(buildExpression).join(", ") + ")" + " " + annotation("/* user-defined function */"); case "function call": return func(node.name) + "(" + ((node.subNodes && node.subNodes[0].subNodes) || [ ]).map(buildExpression).join(", ") + ")"; case "plus": return "(" + binaryOperator(node, "+") + ")"; case "minus": return "(" + binaryOperator(node, "-") + ")"; case "times": return "(" + binaryOperator(node, "*") + ")"; case "division": return "(" + binaryOperator(node, "/") + ")"; case "modulus": return "(" + binaryOperator(node, "%") + ")"; case "compare not in": case "array compare not in": return "(" + binaryOperator(node, "not in") + ")"; case "compare in": case "array compare in": return "(" + binaryOperator(node, "in") + ")"; case "compare ==": case "array compare ==": return "(" + binaryOperator(node, "==") + ")"; case "compare !=": case "array compare !=": return "(" + binaryOperator(node, "!=") + ")"; case "compare >": case "array compare >": return "(" + binaryOperator(node, ">") + ")"; case "compare >=": case "array compare >=": return "(" + binaryOperator(node, ">=") + ")"; case "compare <": case "array compare <": return "(" + binaryOperator(node, "<") + ")"; case "compare <=": case "array compare <=": return "(" + binaryOperator(node, "<=") + ")"; case "logical or": return "(" + binaryOperator(node, "||") + ")"; case "logical and": return "(" + binaryOperator(node, "&&") + ")"; case "ternary": return "(" + buildExpression(node.subNodes[0]) + " ? " + buildExpression(node.subNodes[1]) + " : " + buildExpression(node.subNodes[2]) + ")"; case "n-ary or": if (node.hasOwnProperty("subNodes")) { return bracketize(node, node.subNodes.map(function(sub) { return buildExpression(sub); }).join(" || ")); } return ""; case "n-ary and": if (node.hasOwnProperty("subNodes")) { return bracketize(node, node.subNodes.map(function(sub) { return buildExpression(sub); }).join(" && ")); } return ""; default: return "unhandled node type (" + node.type + ")"; } }; var buildSimpleExpression = function (simpleExpressions) { var rc = ""; for (var indexNo in simpleExpressions) { if (simpleExpressions.hasOwnProperty(indexNo)) { if (rc.length > 0) { rc += " AND "; } for (var i = 0; i < simpleExpressions[indexNo].length; i++) { var item = simpleExpressions[indexNo][i]; rc += attribute("Path") + "."; if (item.isEdgeAccess) { rc += attribute("edges"); } else { rc += attribute("vertices"); } rc += "[" + value(indexNo) + "] -> "; rc += buildExpression(item.varAccess); rc += " " + item.comparisonTypeStr + " "; rc += buildExpression(item.compareTo); } } } return rc; }; var buildBound = function (attr, operators, bound) { var boundValue = bound.isConstant ? value(JSON.stringify(bound.bound)) : buildExpression(bound.bound); return attribute(attr) + " " + operators[bound.include ? 1 : 0] + " " + boundValue; }; var buildRanges = function (ranges) { var results = [ ]; ranges.forEach(function(range) { var attr = range.attr; if (range.lowConst.hasOwnProperty("bound") && range.highConst.hasOwnProperty("bound") && JSON.stringify(range.lowConst.bound) === JSON.stringify(range.highConst.bound)) { range.equality = true; } if (range.equality) { if (range.lowConst.hasOwnProperty("bound")) { results.push(buildBound(attr, [ "==", "==" ], range.lowConst)); } else if (range.hasOwnProperty("lows")) { range.lows.forEach(function(bound) { results.push(buildBound(attr, [ "==", "==" ], bound)); }); } } else { if (range.lowConst.hasOwnProperty("bound")) { results.push(buildBound(attr, [ ">", ">=" ], range.lowConst)); } if (range.highConst.hasOwnProperty("bound")) { results.push(buildBound(attr, [ "<", "<=" ], range.highConst)); } if (range.hasOwnProperty("lows")) { range.lows.forEach(function(bound) { results.push(buildBound(attr, [ ">", ">=" ], bound)); }); } if (range.hasOwnProperty("highs")) { range.highs.forEach(function(bound) { results.push(buildBound(attr, [ "<", "<=" ], bound)); }); } } }); if (results.length > 1) { return "(" + results.join(" && ") + ")"; } return results[0]; }; var label = function (node) { switch (node.type) { case "SingletonNode": return keyword("ROOT"); case "NoResultsNode": return keyword("EMPTY") + " " + annotation("/* empty result set */"); case "EnumerateCollectionNode": collectionVariables[node.outVariable.id] = node.collection; return keyword("FOR") + " " + variableName(node.outVariable) + " " + keyword("IN") + " " + collection(node.collection) + " " + annotation("/* full collection scan" + (node.random ? ", random order" : "") + " */"); case "EnumerateListNode": return keyword("FOR") + " " + variableName(node.outVariable) + " " + keyword("IN") + " " + variableName(node.inVariable) + " " + annotation("/* list iteration */"); case "IndexNode": collectionVariables[node.outVariable.id] = node.collection; var types = [ ]; node.indexes.forEach(function (idx, i) { var what = (node.reverse ? "reverse " : "") + idx.type + " index scan"; if (types.length === 0 || what !== types[types.length - 1]) { types.push(what); } idx.collection = node.collection; idx.node = node.id; if (node.condition.type && node.condition.type === 'n-ary or') { idx.condition = buildExpression(node.condition.subNodes[i]); } else { idx.condition = "*"; // empty condition. this is likely an index used for sorting only } indexes.push(idx); }); return keyword("FOR") + " " + variableName(node.outVariable) + " " + keyword("IN") + " " + collection(node.collection) + " " + annotation("/* " + types.join(", ") + " */"); case "IndexRangeNode": collectionVariables[node.outVariable.id] = node.collection; var index = node.index; index.ranges = node.ranges.map(buildRanges).join(" || "); index.collection = node.collection; index.node = node.id; indexes.push(index); return keyword("FOR") + " " + variableName(node.outVariable) + " " + keyword("IN") + " " + collection(node.collection) + " " + annotation("/* " + (node.reverse ? "reverse " : "") + node.index.type + " index scan */"); case "TraversalNode": node.minMaxDepth = node.minDepth + ".." + node.maxDepth; node.minMaxDepthLen = node.minMaxDepth.length; var rc = keyword("FOR "), parts = []; if (node.hasOwnProperty('vertexOutVariable')) { parts.push(variableName(node.vertexOutVariable) + " " + annotation("/* vertex */")); } if (node.hasOwnProperty('edgeOutVariable')) { parts.push(variableName(node.edgeOutVariable) + " " + annotation("/* edge */")); } if (node.hasOwnProperty('pathOutVariable')) { parts.push(variableName(node.pathOutVariable) + " " + annotation("/* paths */")); } rc += parts.join(", ") + " " + keyword("IN") + " " + value(node.minMaxDepth) + " " + annotation("/* min..maxPathDepth */") + " "; var translate = ["ANY", "INBOUND", "OUTBOUND"]; var defaultDirection = node.directions[0]; rc += keyword(translate[defaultDirection]); if (node.hasOwnProperty("vertexId")) { rc += " '" + value(node.vertexId) + "' "; } else { rc += " " + variableName(node.inVariable) + " "; } rc += annotation("/* startnode */") + " "; if (Array.isArray(node.graph)) { rc += node.graph.map(function(g, index) { var tmp = ""; if (node.directions[index] !== defaultDirection) { tmp += keyword(translate[node.directions[index]]); tmp += " "; } return tmp + collection(g); }).join(", "); } else { rc += keyword("GRAPH") + " '" + value(node.graph) + "'"; } traversalDetails.push(node); if (node.hasOwnProperty('simpleExpressions')) { node.ConditionStr = buildSimpleExpression(node.simpleExpressions); } var e = []; if (node.hasOwnProperty('graphDefinition')) { var v = []; node.graphDefinition.vertexCollectionNames.forEach(function(vcn) { v.push(collection(vcn)); }); node.vertexCollectionNameStr = v.join(", "); node.vertexCollectionNameStrLen = node.graphDefinition.vertexCollectionNames.join(", ").length; node.graphDefinition.edgeCollectionNames.forEach(function(ecn) { e.push(collection(ecn)); }); node.edgeCollectionNameStr = e.join(", "); node.edgeCollectionNameStrLen = node.graphDefinition.edgeCollectionNames.join(", ").length; } else { var edgeCols = node.graph || [ ]; edgeCols.forEach(function(ecn) { e.push(collection(ecn)); }); node.edgeCollectionNameStr = e.join(", "); node.edgeCollectionNameStrLen = edgeCols.join(", ").length; node.graph = ""; } return rc; case "CalculationNode": return keyword("LET") + " " + variableName(node.outVariable) + " = " + buildExpression(node.expression) + " " + annotation("/* " + node.expressionType + " expression */"); case "FilterNode": return keyword("FILTER") + " " + variableName(node.inVariable); case "AggregateNode": /* old-style COLLECT node */ return keyword("COLLECT") + " " + node.aggregates.map(function(node) { return variableName(node.outVariable) + " = " + variableName(node.inVariable); }).join(", ") + (node.count ? " " + keyword("WITH COUNT") : "") + (node.outVariable ? " " + keyword("INTO") + " " + variableName(node.outVariable) : "") + (node.keepVariables ? " " + keyword("KEEP") + " " + node.keepVariables.map(function(variable) { return variableName(variable); }).join(", ") : "") + " " + annotation("/* " + node.aggregationOptions.method + " */"); case "CollectNode": var collect = keyword("COLLECT") + " " + node.groups.map(function(node) { return variableName(node.outVariable) + " = " + variableName(node.inVariable); }).join(", "); if (node.hasOwnProperty("aggregates") && node.aggregates.length > 0) { if (node.groups.length > 0) { collect += " "; } collect += keyword("AGGREGATE") + " " + node.aggregates.map(function(node) { return variableName(node.outVariable) + " = " + func(node.type) + "(" + variableName(node.inVariable) + ")"; }).join(", "); } collect += (node.count ? " " + keyword("WITH COUNT") : "") + (node.outVariable ? " " + keyword("INTO") + " " + variableName(node.outVariable) : "") + (node.expressionVariable ? " = " + variableName(node.expressionVariable) : "") + (node.keepVariables ? " " + keyword("KEEP") + " " + node.keepVariables.map(function(variable) { return variableName(variable.variable); }).join(", ") : "") + " " + annotation("/* " + node.collectOptions.method + "*/"); return collect; case "SortNode": return keyword("SORT") + " " + node.elements.map(function(node) { return variableName(node.inVariable) + " " + keyword(node.ascending ? "ASC" : "DESC"); }).join(", "); case "LimitNode": return keyword("LIMIT") + " " + value(JSON.stringify(node.offset)) + ", " + value(JSON.stringify(node.limit)); case "ReturnNode": return keyword("RETURN") + " " + variableName(node.inVariable); case "SubqueryNode": return keyword("LET") + " " + variableName(node.outVariable) + " = ... " + annotation("/* subquery */"); case "InsertNode": modificationFlags = node.modificationFlags; return keyword("INSERT") + " " + variableName(node.inVariable) + " " + keyword("IN") + " " + collection(node.collection); case "UpdateNode": modificationFlags = node.modificationFlags; if (node.hasOwnProperty("inKeyVariable")) { return keyword("UPDATE") + " " + variableName(node.inKeyVariable) + " " + keyword("WITH") + " " + variableName(node.inDocVariable) + " " + keyword("IN") + " " + collection(node.collection); } return keyword("UPDATE") + " " + variableName(node.inDocVariable) + " " + keyword("IN") + " " + collection(node.collection); case "ReplaceNode": modificationFlags = node.modificationFlags; if (node.hasOwnProperty("inKeyVariable")) { return keyword("REPLACE") + " " + variableName(node.inKeyVariable) + " " + keyword("WITH") + " " + variableName(node.inDocVariable) + " " + keyword("IN") + " " + collection(node.collection); } return keyword("REPLACE") + " " + variableName(node.inDocVariable) + " " + keyword("IN") + " " + collection(node.collection); case "UpsertNode": modificationFlags = node.modificationFlags; return keyword("UPSERT") + " " + variableName(node.inDocVariable) + " " + keyword("INSERT") + " " + variableName(node.insertVariable) + " " + keyword(node.isReplace ? "REPLACE" : "UPDATE") + " " + variableName(node.updateVariable) + " " + keyword("IN") + " " + collection(node.collection); case "RemoveNode": modificationFlags = node.modificationFlags; return keyword("REMOVE") + " " + variableName(node.inVariable) + " " + keyword("IN") + " " + collection(node.collection); case "RemoteNode": return keyword("REMOTE"); case "DistributeNode": return keyword("DISTRIBUTE"); case "ScatterNode": return keyword("SCATTER"); case "GatherNode": return keyword("GATHER"); } return "unhandled node type (" + node.type + ")"; }; var level = 0, subqueries = [ ]; var indent = function (level, isRoot) { return pad(1 + level + level) + (isRoot ? "* " : "- "); }; var preHandle = function (node) { usedVariables = { }; currentNode = node.id; isConst = true; if (node.type === "SubqueryNode") { subqueries.push(level); } }; var postHandle = function (node) { var isLeafNode = ! parents.hasOwnProperty(node.id); if ([ "EnumerateCollectionNode", "EnumerateListNode", "IndexRangeNode", "IndexNode", "SubqueryNode" ].indexOf(node.type) !== -1) { level++; } else if (isLeafNode && subqueries.length > 0) { level = subqueries.pop(); } else if (node.type === "SingletonNode") { level++; } }; var constNess = function () { if (isConst) { return " " + annotation("/* const assignment */"); } return ""; }; var variablesUsed = function () { var used = [ ]; for (var a in usedVariables) { if (usedVariables.hasOwnProperty(a)) { used.push(variable(a) + " : " + collection(usedVariables[a])); } } if (used.length > 0) { return " " + annotation("/* collections used:") + " " + used.join(", ") + " " + annotation("*/"); } return ""; }; var printNode = function (node) { preHandle(node); var line = " " + pad(1 + maxIdLen - String(node.id).length) + variable(node.id) + " " + keyword(node.type) + pad(1 + maxTypeLen - String(node.type).length) + " "; if (cluster && cluster.isCluster && cluster.isCluster()) { line += variable(node.site) + pad(1 + maxSiteLen - String(node.site).length) + " "; } line += pad(1 + maxEstimateLen - String(node.estimatedNrItems).length) + value(node.estimatedNrItems) + " " + indent(level, node.type === "SingletonNode") + label(node); if (node.type === "CalculationNode") { line += variablesUsed() + constNess(); } stringBuilder.appendLine(line); postHandle(node); }; printQuery(query); stringBuilder.appendLine(section("Execution plan:")); var line = " " + pad(1 + maxIdLen - String("Id").length) + header("Id") + " " + header("NodeType") + pad(1 + maxTypeLen - String("NodeType").length) + " "; if (cluster && cluster.isCluster && cluster.isCluster()) { line += header("Site") + pad(1 + maxSiteLen - String("Site").length) + " "; } line += pad(1 + maxEstimateLen - String("Est.").length) + header("Est.") + " " + header("Comment"); stringBuilder.appendLine(line); var walk = [ rootNode ]; while (walk.length > 0) { var id = walk.pop(); var node = nodes[id]; printNode(node); if (parents.hasOwnProperty(id)) { walk = walk.concat(parents[id]); } if (node.type === "SubqueryNode") { walk = walk.concat([ node.subquery.nodes[0].id ]); } } stringBuilder.appendLine(); printIndexes(indexes); printTraversalDetails(traversalDetails); stringBuilder.appendLine(); printRules(plan.rules); printModificationFlags(modificationFlags); printWarnings(explain.warnings); } /* the exposed function */ function explain (data, options, shouldPrint) { 'use strict'; if (typeof data === "string") { data = { query: data }; } if (! (data instanceof Object)) { throw "ArangoStatement needs initial data"; } if (options === undefined) { options = data.options; } options = options || { }; setColors(options.colors === undefined ? true : options.colors); var stmt = db._createStatement(data); var result = stmt.explain(options); stringBuilder.clearOutput(); processQuery(data.query, result, true); if (shouldPrint === undefined || shouldPrint) { print(stringBuilder.getOutput()); } else { return stringBuilder.getOutput(); } } exports.explain = explain;