1
0
Fork 0
arangodb/js/common/modules/@arangodb/aql/explainer.js

1912 lines
68 KiB
JavaScript

/* jshint strict: false, maxlen: 300 */
/* global arango */
var db = require('@arangodb').db,
internal = require('internal'),
_ = require('lodash'),
systemColors = internal.COLORS,
print = internal.print,
colors = {};
// max elements to print from array/objects
const maxMembersToPrint = 20;
let uniqueValue = 0;
const anonymize = function (doc) {
if (Array.isArray(doc)) {
return doc.map(anonymize);
}
if (typeof doc === 'string') {
// make unique values because of unique indexes
return Array(doc.length + 1).join('X') + uniqueValue++;
}
if (doc === null || typeof doc === 'number' || typeof doc === 'boolean') {
return doc;
}
if (typeof doc === 'object') {
let result = {};
Object.keys(doc).forEach(function (key) {
if (key.startsWith('_') || key.startsWith('@')) {
// This excludes system attributes in examples
// and collections in bindVars
result[key] = doc[key];
} else {
result[key] = anonymize(doc[key]);
}
});
return result;
}
return doc;
};
let stringBuilder = {
output: '',
prefix: '',
appendLine: function (line) {
if (line) {
this.output += this.prefix + line;
}
this.output += '\n';
},
getOutput: function () {
return this.output;
},
clearOutput: function () {
this.output = '';
},
wrap: function (str, width) {
let re = '.{1,' + width + '}(\\s|$)|\\S+?(\\s|$)';
return str.match(new RegExp(re, 'g')).join('\n' + this.prefix).replace(/\n+/g, '\n ' + this.prefix);
}
};
/* 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 view(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;
}
// return n times ' '
function pad(n) {
'use strict';
if (n < 0) {
// value seems invalid...
n = 0;
}
return new Array(n).join(' ');
}
/* 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(stringBuilder.wrap(query, 100)));
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 stats */
function printStats(stats) {
'use strict';
if (!stats) {
return;
}
stringBuilder.appendLine(section('Query Statistics:'));
var maxWELen = String('Writes Exec').length;
var maxWILen = String('Writes Ign').length;
var maxSFLen = String('Scan Full').length;
var maxSILen = String('Scan Index').length;
var maxFLen = String('Filtered').length;
var maxETen = String('Exec Time [s]').length;
stats.executionTime = stats.executionTime.toFixed(5);
stringBuilder.appendLine(' ' + header('Writes Exec') + ' ' + header('Writes Ign') + ' ' + header('Scan Full') + ' ' +
header('Scan Index') + ' ' + header('Filtered') + ' ' + header('Exec Time [s]'));
stringBuilder.appendLine(' ' + pad(1 + maxWELen - String(stats.writesExecuted).length) + value(stats.writesExecuted) + ' ' +
pad(1 + maxWILen - String(stats.writesIgnored).length) + value(stats.writesIgnored) + ' ' +
pad(1 + maxSFLen - String(stats.scannedFull).length) + value(stats.scannedFull) + ' ' +
pad(1 + maxSILen - String(stats.scannedIndex).length) + value(stats.scannedIndex) + ' ' +
pad(1 + maxFLen - String(stats.filtered).length) + value(stats.filtered) + ' ' +
pad(1 + maxETen - String(stats.executionTime).length) + value(stats.executionTime));
stringBuilder.appendLine();
}
function printProfile(profile) {
'use strict';
if (!profile) {
return;
}
stringBuilder.appendLine(section('Query Profile:'));
let maxHeadLen = 0;
let maxDurLen = 'Duration [s]'.length;
Object.keys(profile).forEach(key => {
if (key.length > maxHeadLen) {
maxHeadLen = key.length;
}
if (profile[key].toFixed(5).length > maxDurLen) {
maxDurLen = profile[key].toFixed(5).length;
}
});
stringBuilder.appendLine(' ' + header('Query Stage') + pad(1 + maxHeadLen - String('Query Stage').length) + ' ' + pad(1 + maxDurLen - 'Duration [s]'.length) + header('Duration [s]'));
Object.keys(profile).forEach(key => {
stringBuilder.appendLine(' ' + keyword(key) + pad(1 + maxHeadLen - String(key).length) + ' ' + pad(1 + maxDurLen - profile[key].toFixed(5).length) + value(profile[key].toFixed(5)));
});
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 maxNameLen = String('Name').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.name.length;
if (l > maxNameLen) {
maxNameLen = 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('Name') + pad(1 + maxNameLen - 'Name'.length) + ' ' +
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)) + ' ' +
collection(indexes[i].name) + pad(1 + maxNameLen - indexes[i].name.length) + ' ' +
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);
}
}
}
function printFunctions(functions) {
'use strict';
let funcArray = [];
Object.keys(functions).forEach(function (f) {
funcArray.push(functions[f]);
});
if (funcArray.length === 0) {
return;
}
stringBuilder.appendLine();
stringBuilder.appendLine(section('Functions used:'));
let maxNameLen = String('Name').length;
let maxDeterministicLen = String('Deterministic').length;
let maxCacheableLen = String('Cacheable').length;
let maxV8Len = String('Uses V8').length;
funcArray.forEach(function (f) {
let l = String(f.name).length;
if (l > maxNameLen) {
maxNameLen = l;
}
});
let line = ' ' +
header('Name') + pad(1 + maxNameLen - 'Name'.length) + ' ' +
header('Deterministic') + pad(1 + maxDeterministicLen - 'Deterministic'.length) + ' ' +
header('Cacheable') + pad(1 + maxCacheableLen - 'Cacheable'.length) + ' ' +
header('Uses V8') + pad(1 + maxV8Len - 'Uses V8'.length);
stringBuilder.appendLine(line);
for (var i = 0; i < funcArray.length; ++i) {
// prevent "undefined"
let deterministic = String(funcArray[i].isDeterministic || false);
let cacheable = String(funcArray[i].cacheable || false);
let usesV8 = String(funcArray[i].usesV8 || false);
line = ' ' +
variable(funcArray[i].name) + pad(1 + maxNameLen - funcArray[i].name.length) + ' ' +
value(deterministic) + pad(1 + maxDeterministicLen - deterministic.length) + ' ' +
value(cacheable) + pad(1 + maxCacheableLen - cacheable.length) + ' ' +
value(usesV8) + pad(1 + maxV8Len - usesV8.length);
stringBuilder.appendLine(line);
}
}
/* create a table with a given amount of columns and arbitrary many rows */
class PrintedTable {
constructor(numColumns) {
this.content = [];
for (let i = 0; i < numColumns; ++i) {
this.content.push({
header: "",
cells: [],
size: 0
});
}
}
setHeader(index, value) {
this.content[index].header = value;
this.content[index].size = Math.max(this.content[index].size, value.length);
}
addCell(index, value, valueLength) {
// Value might be empty
value = value || "";
valueLength = valueLength || value.length;
this.content[index].cells.push({ formatted: value, size: valueLength });
this.content[index].size = Math.max(this.content[index].size, valueLength);
}
alignNewEntry() {
let rowsNeeded = Math.max(...this.content.map(c => c.cells.length));
for (let c of this.content) {
while (c.cells.length < rowsNeeded) {
c.cells.push({ formatted: '', size: 0 });
}
}
}
print(builder) {
let rowsNeeded = Math.max(...this.content.map(c => c.cells.length));
// Print the header
let line = ' ';
let isFirst = true;
for (let c of this.content) {
line += (isFirst ? '' : pad(3)) + header(c.header) + pad(1 + c.size - c.header.length);
isFirst = false;
}
builder.appendLine(line);
// Print the cells
for (let i = 0; i < rowsNeeded; ++i) {
let line = ' ';
let isFirst = true;
for (let c of this.content) {
if (c.cells.length > i) {
line += (isFirst ? '' : pad(3)) + c.cells[i].formatted + pad(1 + c.size - c.cells[i].size);
} else {
line += (isFirst ? '' : pad(3)) + pad(1 + c.size);
}
isFirst = false;
}
builder.appendLine(line);
}
}
}
/* print traversal info */
function printTraversalDetails(traversals) {
'use strict';
if (traversals.length === 0) {
return;
}
stringBuilder.appendLine();
stringBuilder.appendLine(section('Traversals on graphs:'));
let outTable = new PrintedTable(6);
outTable.setHeader(0, 'Id');
outTable.setHeader(1, 'Depth');
outTable.setHeader(2, 'Vertex collections');
outTable.setHeader(3, 'Edge collections');
outTable.setHeader(4, 'Options');
outTable.setHeader(5, 'Filter / Prune Conditions');
var optify = function (options, colorize) {
var opts = {
bfs: options.bfs || undefined, /* only print if set to true to space room */
uniqueVertices: options.uniqueVertices,
uniqueEdges: options.uniqueEdges
};
var result = '';
for (var att in opts) {
if (result.length > 0) {
result += ', ';
}
if (opts[att] === undefined) {
continue;
}
if (colorize) {
result += keyword(att) + ': ';
if (typeof opts[att] === 'boolean') {
result += value(opts[att] ? 'true' : 'false');
} else {
result += value(String(opts[att]));
}
} else {
result += att + ': ';
if (typeof opts[att] === 'boolean') {
result += opts[att] ? 'true' : 'false';
} else {
result += String(opts[att]);
}
}
}
return result;
};
traversals.forEach(node => {
outTable.alignNewEntry();
outTable.addCell(0, String(node.id));
outTable.addCell(1, node.minMaxDepth);
outTable.addCell(2, node.vertexCollectionNameStr, node.vertexCollectionNameStrLen);
outTable.addCell(3, node.edgeCollectionNameStr, node.edgeCollectionNameStrLen);
if (node.hasOwnProperty('options')) {
outTable.addCell(4, optify(node.options, true), optify(node.options, false).length);
} else if (node.hasOwnProperty('traversalFlags')) {
outTable.addCell(4, optify(node.traversalFlags, true), optify(node.options, false).length);
}
// else do not add a cell in 4
if (node.hasOwnProperty('ConditionStr')) {
outTable.addCell(5, 'FILTER ' + node.ConditionStr);
}
if (node.hasOwnProperty('PruneConditionStr')) {
outTable.addCell(5, 'PRUNE ' + node.PruneConditionStr);
}
});
outTable.print(stringBuilder);
}
/* print shortest_path info */
function printShortestPathDetails(shortestPaths) {
'use strict';
if (shortestPaths.length === 0) {
return;
}
stringBuilder.appendLine();
stringBuilder.appendLine(section('Shortest paths on graphs:'));
var maxIdLen = String('Id').length;
var maxVertexCollectionNameStrLen = String('Vertex collections').length;
var maxEdgeCollectionNameStrLen = String('Edge collections').length;
shortestPaths.forEach(function (node) {
var l = String(node.id).length;
if (l > maxIdLen) {
maxIdLen = l;
}
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('Vertex collections') + pad(1 + maxVertexCollectionNameStrLen - 'Vertex collections'.length) + ' ' +
header('Edge collections') + pad(1 + maxEdgeCollectionNameStrLen - 'Edge collections'.length);
stringBuilder.appendLine(line);
for (let sp of shortestPaths) {
line = ' ' + pad(1 + maxIdLen - String(sp.id).length) + sp.id + ' ';
if (sp.hasOwnProperty('vertexCollectionNameStr')) {
line += sp.vertexCollectionNameStr +
pad(1 + maxVertexCollectionNameStrLen - sp.vertexCollectionNameStrLen) + ' ';
} else {
line += pad(1 + maxVertexCollectionNameStrLen) + ' ';
}
if (sp.hasOwnProperty('edgeCollectionNameStr')) {
line += sp.edgeCollectionNameStr +
pad(1 + maxEdgeCollectionNameStrLen - sp.edgeCollectionNameStrLen) + ' ';
} else {
line += pad(1 + maxEdgeCollectionNameStrLen) + ' ';
}
if (sp.hasOwnProperty('ConditionStr')) {
line += sp.ConditionStr;
}
stringBuilder.appendLine(line);
}
}
/* analyze and print execution plan */
function processQuery(query, explain, planIndex) {
'use strict';
var nodes = {},
parents = {},
rootNode = null,
maxTypeLen = 0,
maxSiteLen = 0,
maxIdLen = String('Id').length,
maxEstimateLen = String('Est.').length,
maxCallsLen = String('Calls').length,
maxItemsLen = String('Items').length,
maxRuntimeLen = String('Runtime [s]').length,
stats = explain.stats;
let plan = explain.plan;
if (planIndex !== undefined) {
plan = explain.plans[planIndex];
}
/// mode with actual runtime stats per node
let profileMode = stats && stats.hasOwnProperty('nodes');
var isCoordinator = false;
if (typeof ArangoClusterComm === 'object') {
isCoordinator = require('@arangodb/cluster').isCoordinator();
} else {
try {
if (arango) {
var result = arango.GET('/_admin/server/role');
if (result.role === 'COORDINATOR') {
isCoordinator = true;
}
}
} catch (err) {
// ignore error
}
}
var recursiveWalk = function (partNodes, level, site) {
let n = _.clone(partNodes);
n.reverse();
n.forEach(function (node) {
// set location of execution node in cluster
node.site = site;
nodes[node.id] = node;
if (level === 0 && node.dependencies.length === 0) {
rootNode = node.id;
}
if (node.type === 'SubqueryNode') {
// enter subquery
recursiveWalk(node.subquery.nodes, level + 1, site);
} else if (node.type === 'RemoteNode') {
site = (site === 'COOR' ? 'DBS' : 'COOR');
}
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 (!profileMode) { // not shown when we got actual runtime stats
if (String(node.estimatedNrItems).length > maxEstimateLen) {
maxEstimateLen = String(node.estimatedNrItems).length;
}
}
});
};
recursiveWalk(plan.nodes, 0, 'COOR');
if (profileMode) { // merge runtime info into plan
stats.nodes.forEach(n => {
if (nodes.hasOwnProperty(n.id)) {
nodes[n.id].calls = n.calls;
nodes[n.id].items = n.items;
nodes[n.id].runtime = n.runtime;
if (String(n.calls).length > maxCallsLen) {
maxCallsLen = String(n.calls).length;
}
if (String(n.items).length > maxItemsLen) {
maxItemsLen = String(n.items).length;
}
let l = String(nodes[n.id].runtime.toFixed(3)).length;
if (l > maxRuntimeLen) {
maxRuntimeLen = l;
}
}
});
// by design the runtime is cumulative right now.
// by subtracting the dependencies from parent runtime we get the runtime per node
stats.nodes.forEach(n => {
if (parents.hasOwnProperty(n.id)) {
parents[n.id].forEach(pid => {
nodes[pid].runtime -= n.runtime;
});
}
});
}
var references = {},
collectionVariables = {},
usedVariables = {},
indexes = [],
traversalDetails = [],
shortestPathDetails = [],
functions = [],
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 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);
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':
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 > maxMembersToPrint) {
// print only the first few values from the object
return '{ ' + node.subNodes.slice(0, maxMembersToPrint).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 > maxMembersToPrint) {
// print only the first few values from the array
return '[ ' + node.subNodes.slice(0, maxMembersToPrint).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':
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 '';
case 'parameter':
case 'datasource parameter':
return value('@' + node.name);
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 projection = function (node) {
if (node.projections && node.projections.length > 0) {
return ', projections: `' + node.projections.join('`, `') + '`';
}
return '';
};
const restriction = function (node) {
if (node.restrictedTo) {
return `, shard: ${node.restrictedTo}`;
} else if (node.numberOfShards) {
return `, ${node.numberOfShards} shard(s)`;
}
return '';
};
var iterateIndexes = function (idx, i, node, types, variable) {
var what = (node.reverse ? 'reverse ' : '') + idx.type + ' index scan' + ((node.producesResult || !node.hasOwnProperty('producesResult')) ? (node.indexCoversProjections ? ', index only' : '') : ', scan only');
if (types.length === 0 || what !== types[types.length - 1]) {
types.push(what);
}
idx.collection = node.collection;
idx.node = node.id;
if (node.hasOwnProperty('condition') && node.condition.type && node.condition.type === 'n-ary or') {
idx.condition = buildExpression(node.condition.subNodes[i]);
} else {
if (variable !== false && variable !== undefined) {
idx.condition = variable;
}
}
if (idx.condition === '' || idx.condition === undefined) {
idx.condition = '*'; // empty condition. this is likely an index used for sorting or scanning only
}
indexes.push(idx);
};
var label = function (node) {
var rc, v, e, edgeCols;
var parts = [];
var types = [];
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' : '') + projection(node) + (node.satellite ? ', satellite' : '') + ((node.producesResult || !node.hasOwnProperty('producesResult')) ? '' : ', scan only') + `${restriction(node)} */`);
case 'EnumerateListNode':
return keyword('FOR') + ' ' + variableName(node.outVariable) + ' ' + keyword('IN') + ' ' + variableName(node.inVariable) + ' ' + annotation('/* list iteration */');
case 'EnumerateViewNode':
var condition = '';
if (node.condition && node.condition.hasOwnProperty('type')) {
condition = ' ' + keyword('SEARCH') + ' ' + buildExpression(node.condition);
}
var scorers = '';
if (node.scorers && node.scorers.length > 0) {
scorers = keyword(' LET ') + node.scorers.map(function (scorer) {
return variableName(scorer) + ' = ' + buildExpression(scorer.node);
}).join(', ');
}
return keyword('FOR') + ' ' + variableName(node.outVariable) + ' ' + keyword('IN') + ' ' + view(node.view) + condition + scorers + ' ' + annotation('/* view query */');
case 'IndexNode':
collectionVariables[node.outVariable.id] = node.collection;
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, false); });
return `${keyword('FOR')} ${variableName(node.outVariable)} ${keyword('IN')} ${collection(node.collection)} ${annotation(`/* ${types.join(', ')}${projection(node)}${node.satellite ? ', satellite' : ''}${restriction(node)}`)} */`;
//`
case 'TraversalNode':
if (node.hasOwnProperty("options")) {
node.minMaxDepth = node.options.minDepth + '..' + node.options.maxDepth;
} else if (node.hasOwnProperty("traversalFlags")) {
node.minMaxDepth = node.traversalFlags.minDepth + '..' + node.traversalFlags.maxDepth;
} else {
node.minMaxDepth = '1..1';
}
node.minMaxDepthLen = node.minMaxDepth.length;
rc = keyword('FOR ');
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 directions = [], d;
for (var i = 0; i < node.edgeCollections.length; ++i) {
var isLast = (i + 1 === node.edgeCollections.length);
d = node.directions[i];
if (!isLast && node.edgeCollections[i] === node.edgeCollections[i + 1]) {
// direction ANY is represented by two traversals: an INBOUND and an OUTBOUND traversal
// on the same collection
d = 0; // ANY
}
directions.push({ collection: node.edgeCollections[i], direction: d });
if (!isLast && node.edgeCollections[i] === node.edgeCollections[i + 1]) {
// don't print same collection twice
++i;
}
}
var allIndexes = [];
for (i = 0; i < node.edgeCollections.length; ++i) {
d = node.directions[i];
// base indexes
var ix = node.indexes.base[i];
ix.collection = node.edgeCollections[i];
ix.condition = keyword("base " + translate[d]);
ix.level = -1;
ix.direction = d;
ix.node = node.id;
allIndexes.push(ix);
// level-specific indexes
for (var l in node.indexes.levels) {
ix = node.indexes.levels[l][i];
ix.collection = node.edgeCollections[i];
ix.condition = keyword("level " + parseInt(l, 10) + " " + translate[d]);
ix.level = parseInt(l, 10);
ix.direction = d;
ix.node = node.id;
allIndexes.push(ix);
}
}
allIndexes.sort(function (l, r) {
if (l.collection !== r.collection) {
return l.collection < r.collection ? -1 : 1;
}
if (l.level !== r.level) {
return l.level < r.level ? -1 : 1;
}
if (l.direction !== r.direction) {
return l.direction < r.direction ? -1 : 1;
}
return 0;
});
rc += keyword(translate[directions[0].direction]);
if (node.hasOwnProperty('vertexId')) {
rc += " '" + value(node.vertexId) + "' ";
} else {
rc += ' ' + variableName(node.inVariable) + ' ';
}
rc += annotation('/* startnode */') + ' ';
if (Array.isArray(node.graph)) {
rc += collection(directions[0].collection);
for (i = 1; i < directions.length; ++i) {
rc += ', ' + keyword(translate[directions[i].direction]) + ' ' + collection(directions[i].collection);
}
} else {
rc += keyword('GRAPH') + " '" + value(node.graph) + "'";
}
traversalDetails.push(node);
if (node.hasOwnProperty('condition')) {
node.ConditionStr = buildExpression(node.condition);
}
if (node.hasOwnProperty('expression')) {
node.PruneConditionStr = buildExpression(node.expression);
}
e = [];
if (node.hasOwnProperty('graphDefinition')) {
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 {
edgeCols = node.graph || [];
edgeCols.forEach(function (ecn) {
e.push(collection(ecn));
});
node.edgeCollectionNameStr = e.join(', ');
node.edgeCollectionNameStrLen = edgeCols.join(', ').length;
node.graph = '<anonymous>';
}
allIndexes.forEach(function (idx) {
indexes.push(idx);
});
return rc;
case 'ShortestPathNode':
if (node.hasOwnProperty('vertexOutVariable')) {
parts.push(variableName(node.vertexOutVariable) + ' ' + annotation('/* vertex */'));
}
if (node.hasOwnProperty('edgeOutVariable')) {
parts.push(variableName(node.edgeOutVariable) + ' ' + annotation('/* edge */'));
}
translate = ['ANY', 'INBOUND', 'OUTBOUND'];
var defaultDirection = node.directions[0];
rc = `${keyword("FOR")} ${parts.join(", ")} ${keyword("IN")} ${keyword(translate[defaultDirection])} ${keyword("SHORTEST_PATH")} `;
if (node.hasOwnProperty('startVertexId')) {
rc += `'${value(node.startVertexId)}'`;
} else {
rc += variableName(node.startInVariable);
}
rc += ` ${annotation("/* startnode */")} ${keyword("TO")} `;
if (node.hasOwnProperty('targetVertexId')) {
rc += `'${value(node.targetVertexId)}'`;
} else {
rc += variableName(node.targetInVariable);
}
rc += ` ${annotation("/* targetnode */")} `;
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) + "'";
}
shortestPathDetails.push(node);
e = [];
if (node.hasOwnProperty('graphDefinition')) {
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 {
edgeCols = node.graph || [];
edgeCols.forEach(function (ecn) {
e.push(collection(ecn));
});
node.edgeCollectionNameStr = e.join(', ');
node.edgeCollectionNameStrLen = edgeCols.join(', ').length;
node.graph = '<anonymous>';
}
return rc;
case 'CalculationNode':
(node.functions || []).forEach(function (f) {
functions[f.name] = f;
});
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 && node.outVariable) ? " = " + 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(', ') + annotation(` /* sorting strategy: ${node.strategy.split("-").join(" ")} */`);
case 'LimitNode':
return keyword('LIMIT') + ' ' + value(JSON.stringify(node.offset)) + ', ' + value(JSON.stringify(node.limit)) + (node.fullCount ? ' ' + annotation('/* fullCount */') : '');
case 'ReturnNode':
return keyword('RETURN') + ' ' + variableName(node.inVariable);
case 'SubqueryNode':
return keyword('LET') + ' ' + variableName(node.outVariable) + ' = ... ' + annotation('/* ' + (node.isConst ? 'const ' : '') + 'subquery */');
case 'InsertNode': {
modificationFlags = node.modificationFlags;
let restrictString = '';
if (node.restrictedTo) {
restrictString = annotation('/* ' + restriction(node) + ' */');
}
return keyword('INSERT') + ' ' + variableName(node.inVariable) + ' ' + keyword('IN') + ' ' + collection(node.collection) + ' ' + restrictString;
}
case 'UpdateNode': {
modificationFlags = node.modificationFlags;
let inputExplain = '';
let indexRef = '';
if (node.hasOwnProperty('inKeyVariable')) {
indexRef = `${variableName(node.inKeyVariable)}`;
inputExplain = `${variableName(node.inKeyVariable)} ${keyword('WITH')} ${variableName(node.inDocVariable)}`;
} else {
indexRef = inputExplain = `variableName(node.inDocVariable)`;
}
let restrictString = '';
if (node.restrictedTo) {
restrictString = annotation('/* ' + restriction(node) + ' */');
}
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
return `${keyword('UPDATE')} ${inputExplain} ${keyword('IN')} ${collection(node.collection)} ${restrictString}`;
}
case 'ReplaceNode': {
modificationFlags = node.modificationFlags;
let inputExplain = '';
let indexRef = '';
if (node.hasOwnProperty('inKeyVariable')) {
indexRef = `${variableName(node.inKeyVariable)}`;
inputExplain = `${variableName(node.inKeyVariable)} ${keyword('WITH')} ${variableName(node.inDocVariable)}`;
} else {
indexRef = inputExplain = `variableName(node.inDocVariable)`;
}
let restrictString = '';
if (node.restrictedTo) {
restrictString = annotation('/* ' + restriction(node) + ' */');
}
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
return `${keyword('REPLACE')} ${inputExplain} ${keyword('IN')} ${collection(node.collection)} ${restrictString}`;
}
case 'UpsertNode':
modificationFlags = node.modificationFlags;
let indexRef = `${variableName(node.inDocVariable)}`;
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
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;
let restrictString = '';
if (node.restrictedTo) {
restrictString = annotation('/* ' + restriction(node) + ' */');
}
let indexRef = `${variableName(node.inVariable)}`;
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
return `${keyword('REMOVE')} ${variableName(node.inVariable)} ${keyword('IN')} ${collection(node.collection)} ${restrictString}`;
}
case 'SingleRemoteOperationNode': {
switch (node.mode) {
case "IndexNode": {
collectionVariables[node.outVariable.id] = node.collection;
let indexRef = `${variable(JSON.stringify(node.key))}`;
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
return `${keyword('FOR')} ${variableName(node.outVariable)} ${keyword('IN')} ${collection(node.collection)} ${keyword('FILTER')} ${variable('_key')} == ${indexRef} ${annotation(`/* primary index scan */`)}`;
// `
}
case 'InsertNode': {
modificationFlags = node.modificationFlags;
collectionVariables[node.inVariable.id] = node.collection;
let indexRef = `${variableName(node.inVariable)}`;
if (node.hasOwnProperty('indexes')) {
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
}
return `${keyword('INSERT')} ${variableName(node.inVariable)} ${keyword('IN')} ${collection(node.collection)}`;
}
case 'UpdateNode': {
modificationFlags = node.modificationFlags;
let OLD = "";
if (node.hasOwnProperty('inVariable')) {
collectionVariables[node.inVariable.id] = node.collection;
OLD = `${keyword('WITH')} ${variableName(node.inVariable)} `;
}
let indexRef;
let keyCondition = "";
let filterCondition;
if (node.hasOwnProperty('key')) {
keyCondition = `{ _key: ${variable(JSON.stringify(node.key))} } `;
indexRef = `${variable(JSON.stringify(node.key))} `;
filterCondition = `${variable('doc._key')} == ${variable(JSON.stringify(node.key))}`;
} else if (node.hasOwnProperty('inVariable')) {
keyCondition = `${variableName(node.inVariable)} `;
indexRef = `${variableName(node.inVariable)}`;
} else {
keyCondition = "<UNSUPPORTED>";
indexRef = "<UNSUPPORTED>";
}
if (node.hasOwnProperty('indexes')) {
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
}
let forStatement = "";
if (node.replaceIndexNode) {
forStatement = `${keyword('FOR')} ${variable('doc')} ${keyword('IN')} ${collection(node.collection)} ${keyword('FILTER')} ${filterCondition} `;
keyCondition = `${variable('doc')} `;
}
return `${forStatement}${keyword('UPDATE')} ${keyCondition}${OLD}${keyword('IN')} ${collection(node.collection)}`;
}
case 'ReplaceNode': {
modificationFlags = node.modificationFlags;
let OLD = "";
if (node.hasOwnProperty('inVariable')) {
collectionVariables[node.inVariable.id] = node.collection;
OLD = `${keyword('WITH')} ${variableName(node.inVariable)} `;
}
let indexRef;
let keyCondition = "";
let filterCondition;
if (node.hasOwnProperty('key')) {
keyCondition = `{ _key: ${variable(JSON.stringify(node.key))} } `;
indexRef = `${variable(JSON.stringify(node.key))}`;
filterCondition = `${variable('doc._key')} == ${variable(JSON.stringify(node.key))}`;
} else if (node.hasOwnProperty('inVariable')) {
keyCondition = `${variableName(node.inVariable)} `;
indexRef = `${variableName(node.inVariable)}`;
} else {
keyCondition = "<UNSUPPORTED>";
indexRef = "<UNSUPPORTED>";
}
if (node.hasOwnProperty('indexes')) {
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
}
let forStatement = "";
if (node.replaceIndexNode) {
forStatement = `${keyword('FOR')} ${variable('doc')} ${keyword('IN')} ${collection(node.collection)} ${keyword('FILTER')} ${filterCondition} `;
keyCondition = `${variable('doc')} `;
}
return `${forStatement}${keyword('REPLACE')} ${keyCondition}${OLD}${keyword('IN')} ${collection(node.collection)}`;
}
case 'RemoveNode': {
modificationFlags = node.modificationFlags;
if (node.hasOwnProperty('inVariable')) {
collectionVariables[node.inVariable.id] = node.collection;
}
let indexRef;
let keyCondition;
let filterCondition;
if (node.hasOwnProperty('key')) {
keyCondition = `{ _key: ${variable(JSON.stringify(node.key))} } `;
indexRef = `${variable(JSON.stringify(node.key))}`;
filterCondition = `${variable('doc._key')} == ${variable(JSON.stringify(node.key))}`;
} else if (node.hasOwnProperty('inVariable')) {
keyCondition = `${variableName(node.inVariable)} `;
indexRef = `${variableName(node.inVariable)}`;
} else {
keyCondition = "<UNSUPPORTED>";
indexRef = "<UNSUPPORTED>";
}
if (node.hasOwnProperty('indexes')) {
node.indexes.forEach(function (idx, i) { iterateIndexes(idx, i, node, types, indexRef); });
}
let forStatement = "";
if (node.replaceIndexNode) {
forStatement = `${keyword('FOR')} ${variable('doc')} ${keyword('IN')} ${collection(node.collection)} ${keyword('FILTER')} ${filterCondition} `;
keyCondition = `${variable('doc')} `;
}
return `${forStatement}${keyword('REMOVE')} ${keyCondition}${keyword('IN')} ${collection(node.collection)}`;
}
}
}
break;
case 'RemoteNode':
return keyword('REMOTE');
case 'DistributeNode':
return keyword('DISTRIBUTE') + ' ' + annotation('/* create keys: ' + node.createKeys + ', variable: ') + variableName(node.variable) + ' ' + annotation('*/');
case 'ScatterNode':
return keyword('SCATTER');
case 'GatherNode':
return keyword('GATHER') + ' ' + node.elements.map(function (node) {
if (node.path && node.path.length) {
return variableName(node.inVariable) + node.path.map(function (n) { return '.' + attribute(n); }) + ' ' + keyword(node.ascending ? 'ASC' : 'DESC');
}
return variableName(node.inVariable) + ' ' + keyword(node.ascending ? 'ASC' : 'DESC');
}).join(', ') + (node.sortmode === 'unset' ? '' : ' ' + annotation('/* sort mode: ' + node.sortmode + ' */'));
}
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',
'EnumerateViewNode',
'IndexRangeNode',
'IndexNode',
'TraversalNode',
'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);
node.runtime = Math.abs(node.runtime);
var line = ' ' +
pad(1 + maxIdLen - String(node.id).length) + variable(node.id) + ' ' +
keyword(node.type) + pad(1 + maxTypeLen - String(node.type).length) + ' ';
if (isCoordinator) {
line += variable(node.site) + pad(1 + maxSiteLen - String(node.site).length) + ' ';
}
if (profileMode) {
line += pad(1 + maxCallsLen - String(node.calls).length) + value(node.calls) + ' ' +
pad(1 + maxItemsLen - String(node.items).length) + value(node.items) + ' ' +
pad(1 + maxRuntimeLen - String(node.runtime.toFixed(5)).length) + value(node.runtime.toFixed(5)) + ' ' +
indent(level, node.type === 'SingletonNode') + label(node);
} else {
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);
};
if (planIndex === undefined) {
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 (isCoordinator) {
line += header('Site') + pad(1 + maxSiteLen - String('Site').length) + ' ';
}
if (profileMode) {
line += pad(1 + maxCallsLen - String('Calls').length) + header('Calls') + ' ' +
pad(1 + maxItemsLen - String('Items').length) + header('Items') + ' ' +
pad(1 + maxRuntimeLen - String('Runtime [s]').length) + header('Runtime [s]') + ' ' +
header('Comment');
} else {
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);
printFunctions(functions);
printTraversalDetails(traversalDetails);
printShortestPathDetails(shortestPathDetails);
stringBuilder.appendLine();
printRules(plan.rules);
printModificationFlags(modificationFlags);
printWarnings(explain.warnings);
if (profileMode) {
printStats(explain.stats);
printProfile(explain.profile);
}
}
/* the exposed explain function */
function explain(data, options, shouldPrint) {
'use strict';
if (typeof data === 'string') {
data = { query: data, options: options };
}
if (!(data instanceof Object)) {
throw 'ArangoStatement needs initial data';
}
if (options === undefined) {
options = data.options;
}
options = options || {};
options.verbosePlans = true;
setColors(options.colors === undefined ? true : options.colors);
stringBuilder.clearOutput();
let stmt = db._createStatement(data);
let result = stmt.explain(options);
if (options.allPlans) {
// multiple plans
printQuery(data.query);
for (let i = 0; i < result.plans.length; ++i) {
if (i > 0) {
stringBuilder.appendLine();
}
stringBuilder.appendLine(section("Plan #" + (i + 1) + " of " + result.plans.length + " (estimated cost: " + result.plans[i].estimatedCost.toFixed(2) + ")"));
stringBuilder.prefix = ' ';
stringBuilder.appendLine();
processQuery(data.query, result, i);
stringBuilder.prefix = '';
}
} else {
// single plan
processQuery(data.query, result, undefined);
}
if (shouldPrint === undefined || shouldPrint) {
print(stringBuilder.getOutput());
} else {
return stringBuilder.getOutput();
}
}
/* the exposed profile query function */
function profileQuery(data, shouldPrint) {
'use strict';
if (!(data instanceof Object) || !data.hasOwnProperty("options")) {
throw 'ArangoStatement needs initial data';
}
let options = data.options || {};
options.silent = true;
options.allPlans = false; // always turn this off, as it will not work with profiling
setColors(options.colors === undefined ? true : options.colors);
stringBuilder.clearOutput();
let stmt = db._createStatement(data);
let cursor = stmt.execute();
let extra = cursor.getExtra();
processQuery(data.query, extra, undefined);
if (shouldPrint === undefined || shouldPrint) {
print(stringBuilder.getOutput());
} else {
return stringBuilder.getOutput();
}
}
/* the exposed debug function */
function debug(query, bindVars, options) {
'use strict';
let input = {};
if (query instanceof Object) {
if (typeof query.toAQL === 'function') {
query = query.toAQL();
}
input = query;
} else {
input.query = query;
if (bindVars !== undefined) {
input.bindVars = anonymize(bindVars);
}
if (options !== undefined) {
input.options = options;
}
}
if (!input.options) {
input.options = {};
}
let result = {
engine: db._engine(),
version: db._version(true),
database: db._name(),
query: input,
collections: {},
views: {}
};
result.fancy = require('@arangodb/aql/explainer').explain(input, { colors: false }, false);
let stmt = db._createStatement(input);
result.explain = stmt.explain(input.options);
let graphs = {};
let collections = result.explain.plan.collections;
let map = {};
collections.forEach(function (c) {
map[c.name] = true;
});
// export graphs
let findGraphs = function (nodes) {
nodes.forEach(function (node) {
if (node.type === 'TraversalNode') {
if (node.graph) {
try {
graphs[node.graph] = db._graphs.document(node.graph);
} catch (err) { }
}
if (node.graphDefinition) {
try {
node.graphDefinition.vertexCollectionNames.forEach(function (c) {
if (!map.hasOwnProperty(c)) {
map[c] = true;
collections.push({ name: c });
}
});
} catch (err) { }
try {
node.graphDefinition.edgeCollectionNames.forEach(function (c) {
if (!map.hasOwnProperty(c)) {
map[c] = true;
collections.push({ name: c });
}
});
} catch (err) { }
}
} else if (node.type === 'SubqueryNode') {
// recurse into subqueries
findGraphs(node.subquery.nodes);
}
});
};
// mangle with graphs used in query
findGraphs(result.explain.plan.nodes);
// add collection information
collections.forEach(function (collection) {
let c = db._collection(collection.name);
if (c === null) {
// probably a view...
let v = db._view(collection.name);
if (v === null) {
return;
}
result.views[collection.name] = {
type: v.type(),
properties: v.properties()
};
} else {
// a collection
let examples;
if (input.options.examples) {
// include example data from collections
let max = 10; // default number of documents
if (typeof input.options.examples === 'number') {
max = input.options.examples;
}
if (max > 100) {
max = 100;
} else if (max < 0) {
max = 0;
}
examples = db._query("FOR doc IN @@collection LIMIT @max RETURN doc", { max, "@collection": collection.name }).toArray();
if (input.options.anonymize) {
examples = examples.map(anonymize);
}
}
result.collections[collection.name] = {
type: c.type(),
properties: c.properties(),
indexes: c.getIndexes(true),
count: c.count(),
counts: c.count(true),
examples
};
}
});
result.graphs = graphs;
return result;
}
function debugDump(filename, query, bindVars, options) {
let result = debug(query, bindVars, options);
require('fs').write(filename, JSON.stringify(result));
require('console').log("stored query debug information in file '" + filename + "'");
}
function inspectDump(filename, outfile) {
let internal = require('internal');
if (outfile !== undefined) {
internal.startCaptureMode();
}
let data = JSON.parse(require('fs').read(filename));
if (data.database) {
print("/* original data gathered from database '" + data.database + "' */");
}
if (db._engine().name !== data.engine.name) {
print("/* using different storage engine (' " + db._engine().name + "') than in debug information ('" + data.engine.name + "') */");
}
print();
print("/* graphs */");
let graphs = data.graphs || {};
Object.keys(graphs).forEach(function (graph) {
let details = graphs[graph];
print("try { db._graphs.remove(" + JSON.stringify(graph) + "); } catch (err) {}");
print("db._graphs.insert(" + JSON.stringify(details) + ");");
});
print();
// all collections and indexes first, as data insertion may go wrong later
print("/* collections and indexes setup */");
Object.keys(data.collections).forEach(function (collection) {
let details = data.collections[collection];
print("db._drop(" + JSON.stringify(collection) + ");");
if (details.type === false || details.type === 3) {
print("db._createEdgeCollection(" + JSON.stringify(collection) + ", " + JSON.stringify(details.properties) + ");");
} else {
print("db._create(" + JSON.stringify(collection) + ", " + JSON.stringify(details.properties) + ");");
}
details.indexes.forEach(function (index) {
delete index.figures;
delete index.selectivityEstimate;
if (index.type !== 'primary' && index.type !== 'edge') {
print("db[" + JSON.stringify(collection) + "].ensureIndex(" + JSON.stringify(index) + ");");
}
});
print();
});
print();
// insert example data
print("/* example data */");
Object.keys(data.collections).forEach(function (collection) {
let details = data.collections[collection];
if (details.examples) {
details.examples.forEach(function (example) {
print("db[" + JSON.stringify(collection) + "].insert(" + JSON.stringify(example) + ");");
});
}
let missing = details.count;
if (details.examples) {
missing -= details.examples.length;
}
if (missing > 0) {
print("/* collection '" + collection + "' needs " + missing + " more document(s) */");
}
print();
});
print();
// views
print("/* views */");
Object.keys(data.views || {}).forEach(function (view) {
let details = data.views[view];
print("db._dropView(" + JSON.stringify(view) + ");");
print("db._createView(" + JSON.stringify(view) + ", " + JSON.stringify(details.type) + ", " + JSON.stringify(details.properties) + ");");
});
print();
print("/* explain result */");
print(data.fancy.trim().split(/\n/).map(function (line) { return "// " + line; }).join("\n"));
print();
print("/* explain command */");
if (data.query.options) {
delete data.query.options.anonymize;
delete data.query.options.colors;
delete data.query.options.examples;
}
print("db._explain(" + JSON.stringify(data.query) + ");");
print();
if (outfile !== undefined) {
require('fs').write(outfile, internal.stopCaptureMode());
require('console').log("stored query restore script in file '" + outfile + "'");
require('console').log("to run it, execute require('internal').load('" + outfile + "');");
}
}
exports.explain = explain;
exports.profileQuery = profileQuery;
exports.debug = debug;
exports.debugDump = debugDump;
exports.inspectDump = inspectDump;