mirror of https://gitee.com/bigwinds/arangodb
579 lines
16 KiB
JavaScript
579 lines
16 KiB
JavaScript
/* jshint strict: false, unused: true, bitwise: false, esnext: true */
|
|
/* global AQL_WARNING */
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief Aql, internal query functions
|
|
// /
|
|
// / @file
|
|
// /
|
|
// / DISCLAIMER
|
|
// /
|
|
// / Copyright 2010-2012 triagens GmbH, Cologne, Germany
|
|
// /
|
|
// / Licensed under the Apache License, Version 2.0 (the "License")
|
|
// / you may not use this file except in compliance with the License.
|
|
// / You may obtain a copy of the License at
|
|
// /
|
|
// / http://www.apache.org/licenses/LICENSE-2.0
|
|
// /
|
|
// / Unless required by applicable law or agreed to in writing, software
|
|
// / distributed under the License is distributed on an "AS IS" BASIS,
|
|
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// / See the License for the specific language governing permissions and
|
|
// / limitations under the License.
|
|
// /
|
|
// / Copyright holder is triAGENS GmbH, Cologne, Germany
|
|
// /
|
|
// / @author Jan Steemann
|
|
// / @author Copyright 2012, triAGENS GmbH, Cologne, Germany
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
var INTERNAL = require('internal');
|
|
var ArangoError = require('@arangodb').ArangoError;
|
|
var isCoordinator = require('@arangodb/cluster').isCoordinator();
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief user functions cache
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
var UserFunctions = { };
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief prefab traversal visitors
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
var DefaultVisitors = {
|
|
'_AQL::HASATTRIBUTESVISITOR': {
|
|
visitorReturnsResults: true,
|
|
func: function (config, result, vertex, path) {
|
|
if (typeof config.data === 'object' && Array.isArray(config.data.attributes)) {
|
|
if (config.data.attributes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var allowNull = true; // default value
|
|
if (config.data.hasOwnProperty('allowNull')) {
|
|
allowNull = config.data.allowNull;
|
|
}
|
|
var i;
|
|
if (config.data.type === 'any') {
|
|
for (i = 0; i < config.data.attributes.length; ++i) {
|
|
if (!vertex.hasOwnProperty(config.data.attributes[i])) {
|
|
continue;
|
|
}
|
|
if (!allowNull && vertex[config.data.attributes[i]] === null) {
|
|
continue;
|
|
}
|
|
|
|
return CLONE({ vertex: vertex, path: path });
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < config.data.attributes.length; ++i) {
|
|
if (!vertex.hasOwnProperty(config.data.attributes[i])) {
|
|
return;
|
|
}
|
|
if (!allowNull &&
|
|
vertex[config.data.attributes[i]] === null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
return CLONE({ vertex: vertex, path: path });
|
|
}
|
|
}
|
|
},
|
|
'_AQL::PROJECTINGVISITOR': {
|
|
visitorReturnsResults: true,
|
|
func: function (config, result, vertex) {
|
|
var values = { };
|
|
if (typeof config.data === 'object' && Array.isArray(config.data.attributes)) {
|
|
config.data.attributes.forEach(function (attribute) {
|
|
values[attribute] = vertex[attribute];
|
|
});
|
|
}
|
|
return values;
|
|
}
|
|
},
|
|
'_AQL::IDVISITOR': {
|
|
visitorReturnsResults: true,
|
|
func: function (config, result, vertex) {
|
|
return vertex._id;
|
|
}
|
|
},
|
|
'_AQL::KEYVISITOR': {
|
|
visitorReturnsResults: true,
|
|
func: function (config, result, vertex) {
|
|
return vertex._key;
|
|
}
|
|
},
|
|
'_AQL::COUNTINGVISITOR': {
|
|
visitorReturnsResults: false,
|
|
func: function (config, result) {
|
|
if (result.length === 0) {
|
|
result.push(0);
|
|
}
|
|
result[0]++;
|
|
}
|
|
}
|
|
};
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief type weight used for sorting and comparing
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
var TYPEWEIGHT_NULL = 0;
|
|
var TYPEWEIGHT_BOOL = 1;
|
|
var TYPEWEIGHT_NUMBER = 2;
|
|
var TYPEWEIGHT_STRING = 4;
|
|
var TYPEWEIGHT_ARRAY = 8;
|
|
var TYPEWEIGHT_OBJECT = 16;
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief raise a warning
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function WARN (func, error, data) {
|
|
'use strict';
|
|
|
|
if (error.code === INTERNAL.errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH.code) {
|
|
AQL_WARNING(error.code, error.message.replace(/%s/, func));
|
|
} else {
|
|
var prefix = '';
|
|
if (func !== null) {
|
|
prefix = "in function '" + func + "()': ";
|
|
}
|
|
|
|
if (typeof data === 'string') {
|
|
AQL_WARNING(error.code, prefix + error.message.replace(/%s/, data));
|
|
} else {
|
|
AQL_WARNING(error.code, prefix + error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief throw a runtime exception
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function THROW (func, error, data, moreMessage) {
|
|
'use strict';
|
|
|
|
var prefix = '';
|
|
if (func !== null && func !== '') {
|
|
prefix = "in function '" + func + "()': ";
|
|
}
|
|
|
|
var err = new ArangoError();
|
|
|
|
err.errorNum = error.code;
|
|
if (typeof data === 'string') {
|
|
err.errorMessage = prefix + error.message.replace(/%s/, data);
|
|
} else {
|
|
err.errorMessage = prefix + error.message;
|
|
}
|
|
if (moreMessage !== undefined) {
|
|
err.errorMessage += '; ' + moreMessage;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief return a database-specific function prefix
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function DB_PREFIX () {
|
|
return INTERNAL.db._name();
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief reset the user functions and reload them from the database
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function reloadUserFunctions () {
|
|
'use strict';
|
|
|
|
var prefix = DB_PREFIX();
|
|
var c = INTERNAL.db._collection('_aqlfunctions');
|
|
|
|
if (c === null) {
|
|
// collection not found. now reset all user functions
|
|
UserFunctions = { };
|
|
UserFunctions[prefix] = { };
|
|
return;
|
|
}
|
|
|
|
var foundError = false;
|
|
var functions = { };
|
|
|
|
c.toArray().forEach(function (f) {
|
|
var key = f._key.replace(/:{1,}/g, '::');
|
|
var code;
|
|
|
|
if (f.code.match(/^\(?function\s+\(/)) {
|
|
code = f.code;
|
|
} else {
|
|
code = '(function() { var callback = ' + f.code + ';\n return callback; })();';
|
|
}
|
|
|
|
try {
|
|
var res = INTERNAL.executeScript(code, undefined, '(user function ' + key + ')');
|
|
if (typeof res !== 'function') {
|
|
foundError = true;
|
|
}
|
|
|
|
functions[key.toUpperCase()] = {
|
|
name: key,
|
|
func: res,
|
|
isDeterministic: f.isDeterministic || false
|
|
};
|
|
} catch (err) {
|
|
// in case a single function is broken, we still continue with the other ones
|
|
// so that at least some functions remain usable
|
|
foundError = true;
|
|
}
|
|
});
|
|
|
|
// now reset the functions for all databases
|
|
// this ensures that functions of other databases will be reloaded next
|
|
// time (the reload does not necessarily need to be carried out in the
|
|
// database in which the function is registered)
|
|
UserFunctions = { };
|
|
UserFunctions[prefix] = functions;
|
|
|
|
if (foundError) {
|
|
THROW(null, INTERNAL.errors.ERROR_QUERY_FUNCTION_INVALID_CODE);
|
|
}
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief get a user-function by name
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function GET_USERFUNCTION (name, config) {
|
|
var prefix = DB_PREFIX(), reloaded = false;
|
|
var key = name.toUpperCase();
|
|
|
|
var func;
|
|
|
|
if (DefaultVisitors.hasOwnProperty(key)) {
|
|
var visitor = DefaultVisitors[key];
|
|
func = visitor.func;
|
|
config.visitorReturnsResults = visitor.visitorReturnsResults;
|
|
} else {
|
|
if (!UserFunctions.hasOwnProperty(prefix)) {
|
|
reloadUserFunctions();
|
|
reloaded = true;
|
|
}
|
|
|
|
if (!UserFunctions[prefix].hasOwnProperty(key) && !reloaded) {
|
|
// last chance
|
|
reloadUserFunctions();
|
|
}
|
|
|
|
if (!UserFunctions[prefix].hasOwnProperty(key)) {
|
|
THROW(null, INTERNAL.errors.ERROR_QUERY_FUNCTION_NOT_FOUND, name);
|
|
}
|
|
|
|
func = UserFunctions[prefix][key].func;
|
|
}
|
|
|
|
if (typeof func !== 'function') {
|
|
THROW(null, INTERNAL.errors.ERROR_QUERY_FUNCTION_NOT_FOUND, name);
|
|
}
|
|
|
|
return func;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief normalize a function name
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function NORMALIZE_FNAME (functionName) {
|
|
'use strict';
|
|
|
|
var p = functionName.indexOf('::');
|
|
|
|
if (p === -1) {
|
|
return functionName;
|
|
}
|
|
|
|
return functionName.substr(p + 2);
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief get access to a collection
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function COLLECTION (name, func) {
|
|
'use strict';
|
|
|
|
if (typeof name !== 'string') {
|
|
THROW(func, INTERNAL.errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH, func);
|
|
}
|
|
|
|
var c;
|
|
if (name.substring(0, 1) === '_') {
|
|
// system collections need to be accessed slightly differently as they
|
|
// are not returned by the propertyGetter of db
|
|
c = INTERNAL.db._collection(name);
|
|
} else {
|
|
c = INTERNAL.db[name];
|
|
}
|
|
|
|
if (c === null || c === undefined) {
|
|
THROW(func, INTERNAL.errors.ERROR_ARANGO_DATA_SOURCE_NOT_FOUND, String(name));
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief clone an object
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function CLONE (obj) {
|
|
'use strict';
|
|
|
|
if (obj === null || typeof (obj) !== 'object') {
|
|
return obj;
|
|
}
|
|
|
|
var copy;
|
|
if (Array.isArray(obj)) {
|
|
copy = [];
|
|
obj.forEach(function (i) {
|
|
copy.push(CLONE(i));
|
|
});
|
|
} else if (obj instanceof Object) {
|
|
copy = { };
|
|
Object.keys(obj).forEach(function (k) {
|
|
copy[k] = CLONE(obj[k]);
|
|
});
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief box a value into the AQL datatype system
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function FIX_VALUE (value) {
|
|
'use strict';
|
|
|
|
var type = typeof (value);
|
|
|
|
if (value === undefined ||
|
|
value === null ||
|
|
(type === 'number' && (isNaN(value) || !isFinite(value)))) {
|
|
return null;
|
|
}
|
|
|
|
if (type === 'boolean' || type === 'string' || type === 'number') {
|
|
return value;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
var i, n = value.length;
|
|
for (i = 0; i < n; ++i) {
|
|
value[i] = FIX_VALUE(value[i]);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
if (type === 'object') {
|
|
var result = { };
|
|
|
|
Object.keys(value).forEach(function (k) {
|
|
if (typeof value[k] !== 'function') {
|
|
result[k] = FIX_VALUE(value[k]);
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief get the sort type of an operand
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function TYPEWEIGHT (value) {
|
|
'use strict';
|
|
|
|
if (value !== undefined && value !== null) {
|
|
if (Array.isArray(value)) {
|
|
return TYPEWEIGHT_ARRAY;
|
|
}
|
|
|
|
switch (typeof (value)) {
|
|
case 'boolean':
|
|
return TYPEWEIGHT_BOOL;
|
|
case 'number':
|
|
if (isNaN(value) || !isFinite(value)) {
|
|
// not a number => null
|
|
return TYPEWEIGHT_NULL;
|
|
}
|
|
return TYPEWEIGHT_NUMBER;
|
|
case 'string':
|
|
return TYPEWEIGHT_STRING;
|
|
case 'object':
|
|
return TYPEWEIGHT_OBJECT;
|
|
}
|
|
}
|
|
|
|
return TYPEWEIGHT_NULL;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief call a user function
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function FCALL_USER (name, parameters, func) {
|
|
'use strict';
|
|
|
|
var prefix = DB_PREFIX(), reloaded = false;
|
|
if (!UserFunctions.hasOwnProperty(prefix)) {
|
|
reloadUserFunctions();
|
|
reloaded = true;
|
|
}
|
|
|
|
if (!UserFunctions[prefix].hasOwnProperty(name) && !reloaded) {
|
|
// last chance
|
|
reloadUserFunctions();
|
|
}
|
|
|
|
if (!UserFunctions[prefix].hasOwnProperty(name)) {
|
|
THROW(func, INTERNAL.errors.ERROR_QUERY_FUNCTION_NOT_FOUND, name);
|
|
}
|
|
|
|
try {
|
|
return FIX_VALUE(UserFunctions[prefix][name].func.apply({ name: name }, parameters));
|
|
} catch (err) {
|
|
THROW(name, INTERNAL.errors.ERROR_QUERY_FUNCTION_RUNTIME_ERROR, AQL_TO_STRING(err.stack || String(err)));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief return the numeric value or undefined if it is out of range
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function NUMERIC_VALUE (value, nullify) {
|
|
'use strict';
|
|
|
|
if (value === null || isNaN(value) || !isFinite(value)) {
|
|
if (nullify) {
|
|
return null;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief cast to a number
|
|
// /
|
|
// / the operand can have any type, returns a number or null
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function AQL_TO_NUMBER (value) {
|
|
'use strict';
|
|
|
|
switch (TYPEWEIGHT(value)) {
|
|
case TYPEWEIGHT_NULL:
|
|
// this covers Infinity and NaN
|
|
return 0;
|
|
case TYPEWEIGHT_BOOL:
|
|
return (value ? 1 : 0);
|
|
case TYPEWEIGHT_NUMBER:
|
|
return value;
|
|
case TYPEWEIGHT_STRING:
|
|
var result = NUMERIC_VALUE(Number(value), false);
|
|
return ((TYPEWEIGHT(result) === TYPEWEIGHT_NUMBER) ? result : null);
|
|
case TYPEWEIGHT_ARRAY:
|
|
if (value.length === 0) {
|
|
return 0;
|
|
}
|
|
if (value.length === 1) {
|
|
return AQL_TO_NUMBER(value[0]);
|
|
}
|
|
// fallthrough intentional
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief cast to a string
|
|
// /
|
|
// / the operand can have any type, always returns a string
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function AQL_TO_STRING (value) {
|
|
'use strict';
|
|
|
|
switch (TYPEWEIGHT(value)) {
|
|
case TYPEWEIGHT_NULL:
|
|
return '';
|
|
case TYPEWEIGHT_BOOL:
|
|
return (value ? 'true' : 'false');
|
|
case TYPEWEIGHT_STRING:
|
|
return value;
|
|
case TYPEWEIGHT_NUMBER:
|
|
return value.toString();
|
|
case TYPEWEIGHT_ARRAY:
|
|
case TYPEWEIGHT_OBJECT:
|
|
return JSON.stringify(value);
|
|
}
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief return documents within a bounding rectangle
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function AQL_WITHIN_RECTANGLE (collection, latitude1, longitude1, latitude2, longitude2) {
|
|
'use strict';
|
|
|
|
if (TYPEWEIGHT(latitude1) !== TYPEWEIGHT_NUMBER ||
|
|
TYPEWEIGHT(longitude1) !== TYPEWEIGHT_NUMBER ||
|
|
TYPEWEIGHT(latitude2) !== TYPEWEIGHT_NUMBER ||
|
|
TYPEWEIGHT(longitude2) !== TYPEWEIGHT_NUMBER) {
|
|
WARN('WITHIN_RECTANGLE', INTERNAL.errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH);
|
|
return null;
|
|
}
|
|
|
|
return COLLECTION(collection, 'WITHIN_RECTANGLE').withinRectangle(
|
|
latitude1,
|
|
longitude1,
|
|
latitude2,
|
|
longitude2
|
|
).toArray();
|
|
}
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief passthru the argument
|
|
// /
|
|
// / this function is marked as non-deterministic so its argument withstands
|
|
// / query optimisation. this function can be used for testing
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
function AQL_PASSTHRU (value) {
|
|
'use strict';
|
|
|
|
return value;
|
|
}
|
|
|
|
exports.FCALL_USER = FCALL_USER;
|
|
exports.AQL_WITHIN_RECTANGLE = AQL_WITHIN_RECTANGLE;
|
|
exports.AQL_V8 = AQL_PASSTHRU;
|
|
|
|
exports.reload = reloadUserFunctions;
|
|
exports.fixValue = FIX_VALUE;
|