mirror of https://gitee.com/bigwinds/arangodb
1912 lines
68 KiB
JavaScript
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;
|