mirror of https://gitee.com/bigwinds/arangodb
1490 lines
43 KiB
JavaScript
1490 lines
43 KiB
JavaScript
/*jshint strict: false */
|
|
/*global ArangoClusterComm, ArangoClusterInfo */
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief Arango Simple Query Language
|
|
///
|
|
/// @file
|
|
///
|
|
/// DISCLAIMER
|
|
///
|
|
/// Copyright 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 Dr. Frank Celler
|
|
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
var internal = require("internal");
|
|
|
|
var ArangoError = require("@arangodb").ArangoError;
|
|
|
|
var sq = require("@arangodb/simple-query-common");
|
|
|
|
var GeneralArrayCursor = sq.GeneralArrayCursor;
|
|
var SimpleQueryAll = sq.SimpleQueryAll;
|
|
var SimpleQueryArray = sq.SimpleQueryArray;
|
|
var SimpleQueryByExample = sq.SimpleQueryByExample;
|
|
var SimpleQueryByCondition = sq.SimpleQueryByCondition;
|
|
var SimpleQueryFulltext = sq.SimpleQueryFulltext;
|
|
var SimpleQueryGeo = sq.SimpleQueryGeo;
|
|
var SimpleQueryNear = sq.SimpleQueryNear;
|
|
var SimpleQueryRange = sq.SimpleQueryRange;
|
|
var SimpleQueryWithin = sq.SimpleQueryWithin;
|
|
var SimpleQueryWithinRectangle = sq.SimpleQueryWithinRectangle;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief rewrites an index id by stripping the collection name from it
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
var rewriteIndex = function (id) {
|
|
if (id === null || id === undefined) {
|
|
return;
|
|
}
|
|
|
|
if (typeof id === "string") {
|
|
return id.replace(/^[a-zA-Z0-9_\-]+\//, '');
|
|
}
|
|
if (typeof id === "object" && id.hasOwnProperty("id")) {
|
|
return id.id.replace(/^[a-zA-Z0-9_\-]+\//, '');
|
|
}
|
|
return id;
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes an all query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryAll.prototype.execute = function () {
|
|
if (this._execution === null) {
|
|
if (this._skip === null) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
var documents;
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/all",
|
|
JSON.stringify({
|
|
collection: shard,
|
|
skip: 0,
|
|
limit: limit || undefined,
|
|
batchSize: 100000000
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
var result = cluster.wait(coord, shards);
|
|
var toSkip = this._skip, toLimit = this._limit;
|
|
|
|
if (toSkip < 0) {
|
|
// negative skip is special
|
|
toLimit = null;
|
|
}
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.count;
|
|
|
|
if (toSkip > 0) {
|
|
if (toSkip >= body.result.length) {
|
|
toSkip -= body.result.length;
|
|
return;
|
|
}
|
|
|
|
body.result = body.result.slice(toSkip);
|
|
toSkip = 0;
|
|
}
|
|
|
|
if (toLimit !== null && toLimit !== undefined) {
|
|
if (body.result.length >= toLimit) {
|
|
body.result = body.result.slice(0, toLimit);
|
|
toLimit = 0;
|
|
}
|
|
else {
|
|
toLimit -= body.result.length;
|
|
}
|
|
}
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (this._skip < 0) {
|
|
// apply negative skip
|
|
var start = _documents.length + this._skip;
|
|
_documents = _documents.slice(start, start + (this._limit || 100000000));
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
documents = this._collection.ALL(this._skip, this._limit);
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents);
|
|
this._countQuery = documents.count;
|
|
this._countTotal = documents.total;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief normalise attribute names for searching in indexes
|
|
/// this will turn
|
|
/// { a: { b: { c: 1, d: true }, e: "bar" } }
|
|
/// into
|
|
/// { "a.b.c" : 1, "a.b.d" : true, "a.e" : "bar" }
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function normalizeAttributes (obj, prefix) {
|
|
var prep = ((prefix === "" || prefix === undefined) ? "" : prefix + "."), o, normalized = { };
|
|
|
|
for (o in obj) {
|
|
if (obj.hasOwnProperty(o)) {
|
|
if (typeof obj[o] === 'object' && ! Array.isArray(obj[o]) && obj[o] !== null) {
|
|
var sub = normalizeAttributes(obj[o], prep + o), i;
|
|
for (i in sub) {
|
|
if (sub.hasOwnProperty(i)) {
|
|
normalized[i] = sub[i];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
normalized[prep + o] = obj[o];
|
|
}
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief whether or not an index supports a query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function supportsQuery (idx, attributes) {
|
|
var matches = 0;
|
|
var fields = idx.fields;
|
|
var n = fields.length;
|
|
for (var i = 0; i < n; ++i) {
|
|
var field = fields[i];
|
|
if (attributes.indexOf(field) === -1) {
|
|
if (idx.type === 'hash') {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
++matches;
|
|
}
|
|
|
|
return (matches > 0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief get the type of a document attribute
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function docType (value) {
|
|
'use strict';
|
|
|
|
if (value !== undefined && value !== null) {
|
|
if (Array.isArray(value)) {
|
|
return 'array';
|
|
}
|
|
|
|
switch (typeof(value)) {
|
|
case 'boolean':
|
|
return 'boolean';
|
|
case 'number':
|
|
if (isNaN(value) || ! isFinite(value)) {
|
|
// not a number => undefined
|
|
return 'null';
|
|
}
|
|
return 'number';
|
|
case 'string':
|
|
return 'string';
|
|
case 'object':
|
|
return 'object';
|
|
}
|
|
}
|
|
|
|
return 'null';
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief checks whether or not the example is contained in the document
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function isContained (doc, example) {
|
|
'use strict';
|
|
|
|
var eType = docType(example);
|
|
var dType = docType(doc);
|
|
if (eType !== dType) {
|
|
return false;
|
|
}
|
|
|
|
var i;
|
|
if (eType === 'object') {
|
|
for (i in example) {
|
|
if (example.hasOwnProperty(i)) {
|
|
if (! doc.hasOwnProperty(i) ||
|
|
! isContained(doc[i], example[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (eType === 'array') {
|
|
if (doc.length !== example.length) {
|
|
return false;
|
|
}
|
|
for (i = 0; i < doc.length; ++i) {
|
|
if (! isContained(doc[i], example[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else if (doc !== example) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief whether or not the example contains null attributes
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function containsNullAttributes (example) {
|
|
var k;
|
|
for (k in example) {
|
|
if (example.hasOwnProperty(k)) {
|
|
if (example[k] === null || example[k] === undefined) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief finds documents by example
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function byExample (data) {
|
|
var k;
|
|
|
|
var collection = data._collection;
|
|
var example = data._example;
|
|
var skip = data._skip;
|
|
var limit = data._limit;
|
|
|
|
if (example === null ||
|
|
typeof example !== "object" ||
|
|
Array.isArray(example)) {
|
|
// invalid datatype for example
|
|
var err1 = new ArangoError();
|
|
err1.errorNum = internal.errors.ERROR_ARANGO_DOCUMENT_TYPE_INVALID.code;
|
|
err1.errorMessage = "invalid document type '" + (typeof example) + "'";
|
|
throw err1;
|
|
}
|
|
|
|
var postFilter = false;
|
|
if (example.hasOwnProperty('_rev')) {
|
|
// the presence of the _rev attribute requires post-filtering
|
|
postFilter = true;
|
|
}
|
|
|
|
var candidates = { total: 0, count: 0, documents: [ ] };
|
|
if (example.hasOwnProperty('_id')) {
|
|
// we can use the collection's primary index
|
|
try {
|
|
candidates.documents = [ collection.document(example._id) ];
|
|
postFilter = true;
|
|
}
|
|
catch (n1) {
|
|
}
|
|
}
|
|
else if (example.hasOwnProperty('_key')) {
|
|
// we can use the collection's primary index
|
|
try {
|
|
candidates.documents = [ collection.document(example._key) ];
|
|
postFilter = true;
|
|
}
|
|
catch (n2) {
|
|
}
|
|
}
|
|
else if (example.hasOwnProperty('_from')) {
|
|
// use edge index
|
|
try {
|
|
candidates.documents = collection.outEdges(example._from);
|
|
postFilter = true;
|
|
}
|
|
catch (n3) {
|
|
}
|
|
}
|
|
else if (example.hasOwnProperty('_to')) {
|
|
// use edge index
|
|
try {
|
|
candidates.documents = collection.inEdges(example._to);
|
|
postFilter = true;
|
|
}
|
|
catch (n4) {
|
|
}
|
|
}
|
|
else {
|
|
// check indexes
|
|
var idx = null;
|
|
var normalized = normalizeAttributes(example, "");
|
|
var keys = Object.keys(normalized);
|
|
var index;
|
|
|
|
if (data._index !== undefined && data._index !== null) {
|
|
if (typeof data._index === 'object' && data._index.hasOwnProperty("id")) {
|
|
index = data._index.id;
|
|
}
|
|
else if (typeof data._index === 'string') {
|
|
index = data._index;
|
|
}
|
|
}
|
|
|
|
if (index !== undefined) {
|
|
// an index was specified
|
|
var all = collection.getIndexes();
|
|
for (k = 0; k < all.length; ++k) {
|
|
if (all[k].type === data._type &&
|
|
rewriteIndex(all[k].id) === rewriteIndex(index)) {
|
|
|
|
if (supportsQuery(all[k], keys)) {
|
|
idx = all[k];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (keys.length > 0) {
|
|
// try these index types
|
|
var checks = [
|
|
{ type: "hash", fields: keys, unique: true },
|
|
{ type: "skiplist", fields: keys, unique: true },
|
|
{ type: "hash", fields: keys, unique: false },
|
|
{ type: "skiplist", fields: keys, unique: false }
|
|
];
|
|
|
|
if (containsNullAttributes(example)) {
|
|
checks.forEach(function(check) {
|
|
check.sparse = false;
|
|
});
|
|
}
|
|
|
|
for (k = 0; k < checks.length; ++k) {
|
|
if (data._type !== undefined && data._type !== checks[k].type) {
|
|
continue;
|
|
}
|
|
|
|
idx = collection.lookupIndex(checks[k]);
|
|
if (idx !== null) {
|
|
// found an index
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (idx !== null) {
|
|
// use an index
|
|
if (idx.type === 'hash') {
|
|
candidates = collection.BY_EXAMPLE_HASH(idx.id, normalized, skip, limit);
|
|
}
|
|
else if (idx.type === 'skiplist') {
|
|
candidates = collection.BY_EXAMPLE_SKIPLIST(idx.id, normalized, skip, limit);
|
|
}
|
|
}
|
|
else {
|
|
if (typeof data._type === "string") {
|
|
// an index was specified, but none can be used
|
|
var err2 = new ArangoError();
|
|
err2.errorNum = internal.errors.ERROR_ARANGO_NO_INDEX.code;
|
|
err2.errorMessage = internal.errors.ERROR_ARANGO_NO_INDEX.message;
|
|
throw err2;
|
|
}
|
|
|
|
if (postFilter) {
|
|
candidates = collection.ALL(skip, limit);
|
|
}
|
|
else {
|
|
candidates = collection.BY_EXAMPLE(example, skip, limit);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (postFilter) {
|
|
var result = [ ];
|
|
|
|
for (k = 0; k < candidates.documents.length; ++k) {
|
|
var doc = candidates.documents[k];
|
|
if (! isContained(doc, example)) {
|
|
continue;
|
|
}
|
|
|
|
if (skip > 0) {
|
|
--skip;
|
|
continue;
|
|
}
|
|
|
|
result.push(doc);
|
|
if (limit > 0 && result.length >= limit) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
candidates.total = candidates.documents.length;
|
|
candidates.count = result.length;
|
|
candidates.documents = result;
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a query-by-example
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryByExample.prototype.execute = function () {
|
|
var documents;
|
|
|
|
if (this._execution === null) {
|
|
if (this._skip === null || this._skip <= 0) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
var method = "by-example";
|
|
if (this._type !== undefined) {
|
|
switch (this._type) {
|
|
case "hash":
|
|
method = "by-example-hash";
|
|
break;
|
|
case "skiplist":
|
|
method = "by-example-skiplist";
|
|
break;
|
|
}
|
|
}
|
|
|
|
var self = this;
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/" + method,
|
|
JSON.stringify({
|
|
example: self._example,
|
|
collection: shard,
|
|
skip: 0,
|
|
limit: limit || undefined,
|
|
batchSize: 100000000,
|
|
index: rewriteIndex(self._index)
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
var result = cluster.wait(coord, shards);
|
|
var toSkip = this._skip, toLimit = this._limit;
|
|
|
|
if (toSkip < 0) {
|
|
// negative skip is special
|
|
toLimit = null;
|
|
}
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.count;
|
|
|
|
if (toSkip > 0) {
|
|
if (toSkip >= body.result.length) {
|
|
toSkip -= body.result.length;
|
|
return;
|
|
}
|
|
|
|
body.result = body.result.slice(toSkip);
|
|
toSkip = 0;
|
|
}
|
|
|
|
if (toLimit !== null && toLimit !== undefined) {
|
|
if (body.result.length >= toLimit) {
|
|
body.result = body.result.slice(0, toLimit);
|
|
toLimit = 0;
|
|
}
|
|
else {
|
|
toLimit -= body.result.length;
|
|
}
|
|
}
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (this._skip < 0) {
|
|
// apply negative skip
|
|
var start = _documents.length + this._skip;
|
|
_documents = _documents.slice(start, start + (this._limit || 100000000));
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
documents = byExample(this);
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents);
|
|
this._countQuery = documents.count;
|
|
this._countTotal = documents.total;
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief query by condition
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function byCondition (data) {
|
|
var collection = data._collection;
|
|
var condition = data._condition;
|
|
var skip = data._skip;
|
|
var limit = data._limit;
|
|
var index;
|
|
|
|
if (data._index !== undefined && data._index !== null) {
|
|
if (typeof data._index === 'object' && data._index.hasOwnProperty("id")) {
|
|
index = data._index.id;
|
|
}
|
|
else if (typeof data._index === 'string') {
|
|
index = data._index;
|
|
}
|
|
}
|
|
else {
|
|
var err1 = new ArangoError();
|
|
err1.errorNum = internal.errors.ERROR_ARANGO_NO_INDEX.code;
|
|
err1.errorMessage = internal.errors.ERROR_ARANGO_NO_INDEX.message;
|
|
throw err1;
|
|
}
|
|
|
|
if (typeof condition !== "object" || Array.isArray(condition)) {
|
|
// invalid datatype for condition
|
|
var err2 = new ArangoError();
|
|
err2.errorNum = internal.errors.ERROR_ARANGO_DOCUMENT_TYPE_INVALID.code;
|
|
err2.errorMessage = "invalid document type";
|
|
throw err2;
|
|
}
|
|
|
|
// always se an index
|
|
switch (data._type) {
|
|
case "skiplist":
|
|
return collection.BY_CONDITION_SKIPLIST(index, condition, skip, limit);
|
|
}
|
|
|
|
// an index type is required, but no index will be used
|
|
var err3 = new ArangoError();
|
|
err3.errorNum = internal.errors.ERROR_ARANGO_NO_INDEX.code;
|
|
err3.errorMessage = internal.errors.ERROR_ARANGO_NO_INDEX.message;
|
|
throw err3;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a query-by-condition
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryByCondition.prototype.execute = function () {
|
|
var documents;
|
|
|
|
if (this._execution === null) {
|
|
if (this._skip === null || this._skip <= 0) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
var method;
|
|
if (this._type !== undefined) {
|
|
switch (this._type) {
|
|
case "skiplist":
|
|
method = "by-condition-skiplist";
|
|
break;
|
|
}
|
|
}
|
|
|
|
var self = this;
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/" + method,
|
|
JSON.stringify({
|
|
condition: self._condition,
|
|
collection: shard,
|
|
skip: 0,
|
|
limit: limit || undefined,
|
|
batchSize: 100000000,
|
|
index: rewriteIndex(self._index)
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
var result = cluster.wait(coord, shards);
|
|
var toSkip = this._skip, toLimit = this._limit;
|
|
|
|
if (toSkip < 0) {
|
|
// negative skip is special
|
|
toLimit = null;
|
|
}
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.count;
|
|
|
|
if (toSkip > 0) {
|
|
if (toSkip >= body.result.length) {
|
|
toSkip -= body.result.length;
|
|
return;
|
|
}
|
|
|
|
body.result = body.result.slice(toSkip);
|
|
toSkip = 0;
|
|
}
|
|
|
|
if (toLimit !== null && toLimit !== undefined) {
|
|
if (body.result.length >= toLimit) {
|
|
body.result = body.result.slice(0, toLimit);
|
|
toLimit = 0;
|
|
}
|
|
else {
|
|
toLimit -= body.result.length;
|
|
}
|
|
}
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (this._skip < 0) {
|
|
// apply negative skip
|
|
var start = _documents.length + this._skip;
|
|
_documents = _documents.slice(start, start + (this._limit || 100000000));
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
documents = byCondition(this);
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents);
|
|
this._countQuery = documents.count;
|
|
this._countTotal = documents.total;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief ranged query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function rangedQuery (collection, attribute, left, right, type, skip, limit) {
|
|
var documents;
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var _limit = 0;
|
|
if (limit > 0) {
|
|
if (skip >= 0) {
|
|
_limit = skip + limit;
|
|
}
|
|
}
|
|
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/range",
|
|
JSON.stringify({
|
|
collection: shard,
|
|
attribute: attribute,
|
|
left: left,
|
|
right: right,
|
|
closed: type,
|
|
skip: 0,
|
|
limit: _limit || undefined,
|
|
batchSize: 100000000
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
var result = cluster.wait(coord, shards);
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.count;
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (shards.length > 1) {
|
|
var cmp = require("@arangodb/aql").RELATIONAL_CMP;
|
|
_documents.sort(function (l, r) {
|
|
return cmp(l[attribute], r[attribute]);
|
|
});
|
|
}
|
|
|
|
if (limit > 0 && skip >= 0) {
|
|
_documents = _documents.slice(skip, skip + limit);
|
|
}
|
|
else if (skip > 0) {
|
|
_documents = _documents.slice(skip, _documents.length);
|
|
}
|
|
else if (skip < 0) {
|
|
// apply negative skip
|
|
var start = _documents.length + skip;
|
|
_documents = _documents.slice(start, start + (limit || 100000000));
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
var idx = null;
|
|
var attrs = { type: "skiplist", fields: [ attribute ], unique: true };
|
|
if (left === undefined || left === null) {
|
|
attrs.sparse = false;
|
|
}
|
|
|
|
idx = collection.lookupIndex(attrs);
|
|
if (idx === null) {
|
|
attrs.unique = false;
|
|
idx = collection.lookupIndex(attrs);
|
|
}
|
|
|
|
if (idx !== null) {
|
|
var cond = {};
|
|
|
|
if (type === 0) {
|
|
cond[attribute] = [ [ ">=", left ], [ "<", right ] ];
|
|
}
|
|
else if (type === 1) {
|
|
cond[attribute] = [ [ ">=", left ], [ "<=", right ] ];
|
|
}
|
|
else {
|
|
throw "unknown type";
|
|
}
|
|
|
|
documents = collection.BY_CONDITION_SKIPLIST(idx.id, cond, skip, limit);
|
|
}
|
|
else {
|
|
var err = new ArangoError();
|
|
err.errorNum = internal.errors.ERROR_ARANGO_NO_INDEX.code;
|
|
err.errorMessage = internal.errors.ERROR_ARANGO_NO_INDEX.message;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return documents;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a range query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryRange.prototype.execute = function () {
|
|
var documents;
|
|
|
|
if (this._execution === null) {
|
|
if (this._skip === null) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
documents = rangedQuery(this._collection,
|
|
this._attribute,
|
|
this._left,
|
|
this._right,
|
|
this._type,
|
|
this._skip,
|
|
this._limit);
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents);
|
|
this._countQuery = documents.count;
|
|
this._countTotal = documents.total;
|
|
}
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a near query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryNear.prototype.execute = function () {
|
|
var documents;
|
|
var result;
|
|
var limit;
|
|
var i, n;
|
|
|
|
if (this._execution !== null) {
|
|
return;
|
|
}
|
|
|
|
if (this._skip === null) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
if (this._skip < 0) {
|
|
var err = new ArangoError();
|
|
err.errorNum = internal.errors.ERROR_BAD_PARAMETER.code;
|
|
err.errorMessage = "skip must be non-negative";
|
|
throw err;
|
|
}
|
|
|
|
if (this._limit === null) {
|
|
limit = this._skip + 100;
|
|
}
|
|
else {
|
|
limit = this._skip + this._limit;
|
|
}
|
|
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var _limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
_limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
var attribute;
|
|
if (this._distance !== null) {
|
|
attribute = this._distance;
|
|
}
|
|
else {
|
|
// use a pseudo-attribute for distance (we need this for sorting)
|
|
attribute = "$distance";
|
|
}
|
|
|
|
var self = this;
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/near",
|
|
JSON.stringify({
|
|
collection: shard,
|
|
latitude: self._latitude,
|
|
longitude: self._longitude,
|
|
distance: attribute,
|
|
geo: rewriteIndex(self._index),
|
|
skip: 0,
|
|
limit: _limit || undefined,
|
|
batchSize: 100000000
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
result = cluster.wait(coord, shards);
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.total;
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (shards.length > 1) {
|
|
_documents.sort(function (l, r) {
|
|
if (l[attribute] === r[attribute]) {
|
|
return 0;
|
|
}
|
|
return (l[attribute] < r[attribute] ? -1 : 1);
|
|
});
|
|
}
|
|
|
|
if (this._limit > 0) {
|
|
_documents = _documents.slice(0, this._skip + this._limit);
|
|
}
|
|
|
|
if (this._distance === null) {
|
|
n = _documents.length;
|
|
for (i = 0; i < n; ++i) {
|
|
delete _documents[i][attribute];
|
|
}
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
result = this._collection.NEAR(this._index, this._latitude, this._longitude, limit);
|
|
|
|
documents = {
|
|
documents: result.documents,
|
|
count: result.documents.length,
|
|
total: result.documents.length
|
|
};
|
|
|
|
if (this._distance !== null) {
|
|
var distances = result.distances;
|
|
n = documents.documents.length;
|
|
for (i = this._skip; i < n; ++i) {
|
|
documents.documents[i][this._distance] = distances[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents, this._skip, null);
|
|
this._countQuery = documents.total - this._skip;
|
|
this._countTotal = documents.total;
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a within query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryWithin.prototype.execute = function () {
|
|
var result;
|
|
var documents;
|
|
var i, n;
|
|
|
|
if (this._execution !== null) {
|
|
return;
|
|
}
|
|
|
|
if (this._skip === null) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
if (this._skip < 0) {
|
|
var err = new ArangoError();
|
|
err.errorNum = internal.errors.ERROR_BAD_PARAMETER.code;
|
|
err.errorMessage = "skip must be non-negative";
|
|
throw err;
|
|
}
|
|
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var _limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
_limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
var attribute;
|
|
if (this._distance !== null) {
|
|
attribute = this._distance;
|
|
}
|
|
else {
|
|
// use a pseudo-attribute for distance (we need this for sorting)
|
|
attribute = "$distance";
|
|
}
|
|
|
|
var self = this;
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/within",
|
|
JSON.stringify({
|
|
collection: shard,
|
|
latitude: self._latitude,
|
|
longitude: self._longitude,
|
|
distance: attribute,
|
|
radius: self._radius,
|
|
geo: rewriteIndex(self._index),
|
|
skip: 0,
|
|
limit: _limit || undefined,
|
|
batchSize: 100000000
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
result = cluster.wait(coord, shards);
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.total;
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (shards.length > 1) {
|
|
_documents.sort(function (l, r) {
|
|
if (l[attribute] === r[attribute]) {
|
|
return 0;
|
|
}
|
|
return (l[attribute] < r[attribute] ? -1 : 1);
|
|
});
|
|
}
|
|
|
|
if (this._limit > 0) {
|
|
_documents = _documents.slice(0, this._skip + this._limit);
|
|
}
|
|
|
|
if (this._distance === null) {
|
|
n = _documents.length;
|
|
for (i = 0; i < n; ++i) {
|
|
delete _documents[i][attribute];
|
|
}
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
result = this._collection.WITHIN(this._index, this._latitude, this._longitude, this._radius);
|
|
|
|
documents = {
|
|
documents: result.documents,
|
|
count: result.documents.length,
|
|
total: result.documents.length
|
|
};
|
|
|
|
if (this._limit > 0) {
|
|
documents.documents = documents.documents.slice(0, this._skip + this._limit);
|
|
}
|
|
|
|
if (this._distance !== null) {
|
|
var distances = result.distances;
|
|
n = documents.documents.length;
|
|
for (i = this._skip; i < n; ++i) {
|
|
documents.documents[i][this._distance] = distances[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents, this._skip, null);
|
|
this._countQuery = documents.total - this._skip;
|
|
this._countTotal = documents.total;
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a within-rectangle query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryWithinRectangle.prototype.execute = function () {
|
|
var result;
|
|
var documents;
|
|
|
|
if (this._execution !== null) {
|
|
return;
|
|
}
|
|
|
|
if (this._skip === null) {
|
|
this._skip = 0;
|
|
}
|
|
|
|
if (this._skip < 0) {
|
|
var err = new ArangoError();
|
|
err.errorNum = internal.errors.ERROR_BAD_PARAMETER.code;
|
|
err.errorMessage = "skip must be non-negative";
|
|
throw err;
|
|
}
|
|
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var _limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
_limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
var self = this;
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/within-rectangle",
|
|
JSON.stringify({
|
|
collection: shard,
|
|
latitude1: self._latitude1,
|
|
longitude1: self._longitude1,
|
|
latitude2: self._latitude2,
|
|
longitude2: self._longitude2,
|
|
geo: rewriteIndex(self._index),
|
|
skip: 0,
|
|
limit: _limit || undefined,
|
|
batchSize: 100000000
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
result = cluster.wait(coord, shards);
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.total;
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (this._limit > 0) {
|
|
_documents = _documents.slice(0, this._skip + this._limit);
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
var distanceMeters = function (lat1, lon1, lat2, lon2) {
|
|
var deltaLat = (lat2 - lat1) * Math.PI / 180;
|
|
var deltaLon = (lon2 - lon1) * Math.PI / 180;
|
|
var a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
|
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
|
|
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
return 6378.137 /* radius of earth in kilometers */
|
|
* c
|
|
* 1000; // kilometers to meters;
|
|
};
|
|
|
|
var diameter = distanceMeters(this._latitude1, this._longitude1, this._latitude2, this._longitude2);
|
|
var midpoint = [
|
|
this._latitude1 + (this._latitude2 - this._latitude1) * 0.5,
|
|
this._longitude1 + (this._longitude2 - this._longitude1) * 0.5
|
|
];
|
|
|
|
result = this._collection.WITHIN(this._index, midpoint[0], midpoint[1], diameter);
|
|
|
|
var idx = this._collection.index(this._index);
|
|
var latLower, latUpper, lonLower, lonUpper;
|
|
|
|
if (this._latitude1 < this._latitude2) {
|
|
latLower = this._latitude1;
|
|
latUpper = this._latitude2;
|
|
}
|
|
else {
|
|
latLower = this._latitude2;
|
|
latUpper = this._latitude1;
|
|
}
|
|
|
|
if (this._longitude1 < this._longitude2) {
|
|
lonLower = this._longitude1;
|
|
lonUpper = this._longitude2;
|
|
}
|
|
else {
|
|
lonLower = this._longitude2;
|
|
lonUpper = this._longitude1;
|
|
}
|
|
|
|
var deref = function(doc, parts) {
|
|
if (parts.length === 1) {
|
|
return doc[parts[0]];
|
|
}
|
|
|
|
var i = 0;
|
|
try {
|
|
while (i < parts.length && doc !== null && doc !== undefined) {
|
|
doc = doc[parts[i]];
|
|
++i;
|
|
}
|
|
return doc;
|
|
}
|
|
catch (err) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
documents = [ ];
|
|
if (idx.type === 'geo1') {
|
|
// geo1, we have both coordinates in a list
|
|
var attribute = idx.fields[0];
|
|
var parts = attribute.split(".");
|
|
|
|
if (idx.geoJson) {
|
|
result.documents.forEach(function(document) {
|
|
var doc = deref(document, parts);
|
|
if (! Array.isArray(doc)) {
|
|
return;
|
|
}
|
|
|
|
// check if within bounding rectangle
|
|
// first list value is longitude, then latitude
|
|
if (doc[1] >= latLower && doc[1] <= latUpper &&
|
|
doc[0] >= lonLower && doc[0] <= lonUpper) {
|
|
documents.push(document);
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
result.documents.forEach(function(document) {
|
|
var doc = deref(document, parts);
|
|
if (! Array.isArray(doc)) {
|
|
return;
|
|
}
|
|
|
|
// check if within bounding rectangle
|
|
// first list value is latitude, then longitude
|
|
if (doc[0] >= latLower && doc[0] <= latUpper &&
|
|
doc[1] >= lonLower && doc[1] <= lonUpper) {
|
|
documents.push(document);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// geo2, we have dedicated latitude and longitude attributes
|
|
var latAtt = idx.fields[0], lonAtt = idx.fields[1];
|
|
var latParts = latAtt.split(".");
|
|
var lonParts = lonAtt.split(".");
|
|
|
|
result.documents.forEach(function(document) {
|
|
var latDoc = deref(document, latParts);
|
|
if (latDoc === null || latDoc === undefined) {
|
|
return;
|
|
}
|
|
var lonDoc = deref(document, lonParts);
|
|
if (lonDoc === null || lonDoc === undefined) {
|
|
return;
|
|
}
|
|
|
|
// check if within bounding rectangle
|
|
if (latDoc >= latLower && latDoc <= latUpper &&
|
|
lonDoc >= lonLower && lonDoc <= lonUpper) {
|
|
documents.push(document);
|
|
}
|
|
});
|
|
}
|
|
|
|
documents = {
|
|
documents: documents,
|
|
count: result.documents.length,
|
|
total: result.documents.length
|
|
};
|
|
|
|
if (this._limit > 0) {
|
|
documents.documents = documents.documents.slice(0, this._skip + this._limit);
|
|
documents.count = documents.documents.length;
|
|
}
|
|
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents, this._skip, null);
|
|
this._countQuery = documents.total - this._skip;
|
|
this._countTotal = documents.total;
|
|
};
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief executes a fulltext query
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
SimpleQueryFulltext.prototype.execute = function () {
|
|
var result;
|
|
var documents;
|
|
|
|
if (this._execution !== null) {
|
|
return;
|
|
}
|
|
|
|
var cluster = require("@arangodb/cluster");
|
|
|
|
if (cluster.isCoordinator()) {
|
|
var dbName = require("internal").db._name();
|
|
var shards = cluster.shardList(dbName, this._collection.name());
|
|
var coord = { coordTransactionID: ArangoClusterInfo.uniqid() };
|
|
var options = { coordTransactionID: coord.coordTransactionID, timeout: 360 };
|
|
var _limit = 0;
|
|
if (this._limit > 0) {
|
|
if (this._skip >= 0) {
|
|
_limit = this._skip + this._limit;
|
|
}
|
|
}
|
|
|
|
var self = this;
|
|
shards.forEach(function (shard) {
|
|
ArangoClusterComm.asyncRequest("put",
|
|
"shard:" + shard,
|
|
dbName,
|
|
"/_api/simple/fulltext",
|
|
JSON.stringify({
|
|
collection: shard,
|
|
attribute: self._attribute,
|
|
query: self._query,
|
|
index: rewriteIndex(self._index),
|
|
skip: 0,
|
|
limit: _limit || undefined,
|
|
batchSize: 100000000
|
|
}),
|
|
{ },
|
|
options);
|
|
});
|
|
|
|
var _documents = [ ], total = 0;
|
|
result = cluster.wait(coord, shards);
|
|
|
|
result.forEach(function(part) {
|
|
var body = JSON.parse(part.body);
|
|
total += body.total;
|
|
|
|
_documents = _documents.concat(body.result);
|
|
});
|
|
|
|
if (this._limit > 0) {
|
|
_documents = _documents.slice(0, this._skip + this._limit);
|
|
}
|
|
|
|
documents = {
|
|
documents: _documents,
|
|
count: _documents.length,
|
|
total: total
|
|
};
|
|
}
|
|
else {
|
|
result = this._collection.FULLTEXT(this._index, this._query);
|
|
|
|
documents = {
|
|
documents: result.documents,
|
|
count: result.documents.length - this._skip,
|
|
total: result.documents.length
|
|
};
|
|
|
|
if (this._limit > 0) {
|
|
documents.documents = documents.documents.slice(0, this._skip + this._limit);
|
|
}
|
|
}
|
|
|
|
this._execution = new GeneralArrayCursor(documents.documents, this._skip, null);
|
|
this._countQuery = documents.total - this._skip;
|
|
this._countTotal = documents.total;
|
|
};
|
|
|
|
|
|
exports.GeneralArrayCursor = GeneralArrayCursor;
|
|
exports.SimpleQueryAll = SimpleQueryAll;
|
|
exports.SimpleQueryArray = SimpleQueryArray;
|
|
exports.SimpleQueryByExample = SimpleQueryByExample;
|
|
exports.SimpleQueryByCondition = SimpleQueryByCondition;
|
|
exports.SimpleQueryFulltext = SimpleQueryFulltext;
|
|
exports.SimpleQueryGeo = SimpleQueryGeo;
|
|
exports.SimpleQueryNear = SimpleQueryNear;
|
|
exports.SimpleQueryRange = SimpleQueryRange;
|
|
exports.SimpleQueryWithin = SimpleQueryWithin;
|
|
exports.SimpleQueryWithinRectangle = SimpleQueryWithinRectangle;
|
|
exports.byExample = byExample;
|
|
|
|
|