1
0
Fork 0
arangodb/js/server/modules/@arangodb/simple-query.js

558 lines
17 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 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 build a limit string
////////////////////////////////////////////////////////////////////////////////
var limitString = function (skip, limit) {
if (skip > 0 || limit > 0) {
if (limit <= 0) {
limit = 99999999999;
}
return "LIMIT " + parseInt(skip, 10) + ", " + parseInt(limit, 10) + " ";
}
return "";
};
////////////////////////////////////////////////////////////////////////////////
/// @brief executes an all query
////////////////////////////////////////////////////////////////////////////////
SimpleQueryAll.prototype.execute = function () {
if (this._execution !== null) {
return;
}
if (this._skip === null) {
this._skip = 0;
}
var bindVars = { "@collection": this._collection.name() };
var query = "FOR doc IN @@collection " + limitString(this._skip, this._limit) + " RETURN doc";
var documents = require("internal").db._query({ query, bindVars }).toArray();
this._execution = new GeneralArrayCursor(documents);
this._countQuery = documents.length;
this._countTotal = documents.length;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief executes a query-by-example
////////////////////////////////////////////////////////////////////////////////
SimpleQueryByExample.prototype.execute = function () {
if (this._execution !== null) {
return;
}
if (this._skip === null || this._skip <= 0) {
this._skip = 0;
}
if (this._example === null ||
typeof this._example !== "object" ||
Array.isArray(this._example)) {
// invalid datatype for example
var err = new ArangoError();
err.errorNum = internal.errors.ERROR_ARANGO_DOCUMENT_TYPE_INVALID.code;
err.errorMessage = "invalid document type '" + (typeof this._example) + "'";
throw err;
}
var bindVars = { "@collection" : this._collection.name() };
var filters = [];
var self = this;
Object.keys(this._example).forEach(function(key) {
var value = self._example[key];
filters.push("FILTER doc.`" + key.replace(/\`/g, '').split(".").join("`.`") + "` == " + JSON.stringify(value));
});
var query = "FOR doc IN @@collection " + filters.join(" ") + " " +
limitString(this._skip, this._limit) + " RETURN doc";
var documents = require("internal").db._query({ query, bindVars }).toArray();
this._execution = new GeneralArrayCursor(documents);
this._countQuery = documents.length;
this._countTotal = documents.length;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief executes a range query
////////////////////////////////////////////////////////////////////////////////
SimpleQueryRange.prototype.execute = function () {
if (this._execution !== null) {
return;
}
var query = "FOR doc IN @@collection ";
var bindVars = {
"@collection": this._collection.name(),
attribute: this._attribute,
left: this._left,
right: this._right
};
if (this._type === 0) {
query += "FILTER doc.@attribute >= @left && doc.@attribute < @right ";
}
else if (this._type === 1) {
query += "FILTER doc.@attribute >= @left && doc.@attribute <= @right ";
}
else {
throw "unknown type";
}
query += limitString(this._skip, this._limit) + " RETURN doc";
var documents = require("internal").db._query({ query, bindVars }).toArray();
this._execution = new GeneralArrayCursor(documents);
this._countQuery = documents.length - this._skip;
this._countTotal = documents.length;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief executes a near query
////////////////////////////////////////////////////////////////////////////////
SimpleQueryNear.prototype.execute = function () {
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 <= 0) {
this._limit = 100;
}
var bindVars = {
"@collection": this._collection.name(),
latitude: this._latitude,
longitude: this._longitude,
limit: parseInt(this._limit + this._skip, 10)
};
var mustSort = false;
if (require("@arangodb/cluster").isCoordinator()) {
if (this._distance === null || this._distance === undefined) {
this._distance = "_distance";
}
mustSort = true;
}
var query;
if (typeof this._distance === 'string') {
query = "FOR doc IN NEAR(@@collection, @latitude, @longitude, @limit, @distance) ";
bindVars.distance = this._distance;
}
else {
query = "FOR doc IN NEAR(@@collection, @latitude, @longitude, @limit) ";
}
if (mustSort) {
query += "SORT doc.@distance ";
}
query += limitString(this._skip, this._limit) + " RETURN doc";
var documents = require("internal").db._query({ query, bindVars }).toArray();
this._execution = new GeneralArrayCursor(documents);
this._countQuery = documents.length - this._skip;
this._countTotal = documents.length;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief executes a within query
////////////////////////////////////////////////////////////////////////////////
SimpleQueryWithin.prototype.execute = function () {
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 bindVars = {
"@collection": this._collection.name(),
latitude: this._latitude,
longitude: this._longitude,
radius: this._radius
};
var mustSort = false;
if (require("@arangodb/cluster").isCoordinator()) {
if (this._distance === null || this._distance === undefined) {
this._distance = "_distance";
}
mustSort = true;
}
var query;
if (typeof this._distance === 'string') {
query = "FOR doc IN WITHIN(@@collection, @latitude, @longitude, @radius, @distance) ";
bindVars.distance = this._distance;
}
else {
query = "FOR doc IN WITHIN(@@collection, @latitude, @longitude, @radius) ";
}
if (mustSort) {
query += "SORT doc.@distance ";
}
query += limitString(this._skip, this._limit) + " RETURN doc";
var documents = require("internal").db._query({ query, bindVars }).toArray();
this._execution = new GeneralArrayCursor(documents);
this._countQuery = documents.length - this._skip;
this._countTotal = documents.length;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief executes a fulltext query
////////////////////////////////////////////////////////////////////////////////
SimpleQueryFulltext.prototype.execute = function () {
if (this._execution !== null) {
return;
}
var limit = 0;
if (this._limit > 0) {
limit = this._limit;
}
var bindVars = {
"@collection": this._collection.name(),
attribute: this._attribute,
query: this._query,
limit: limit
};
var query = "FOR doc IN FULLTEXT(@@collection, @attribute, @query, @limit) " +
limitString(this._skip, this._limit) + " RETURN doc";
var documents = require("internal").db._query({ query, bindVars }).toArray();
this._execution = new GeneralArrayCursor(documents);
this._countQuery = documents.length - this._skip;
this._countTotal = documents.length;
};
////////////////////////////////////////////////////////////////////////////////
/// @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;
};
exports.GeneralArrayCursor = GeneralArrayCursor;
exports.SimpleQueryAll = SimpleQueryAll;
exports.SimpleQueryArray = SimpleQueryArray;
exports.SimpleQueryByExample = SimpleQueryByExample;
exports.SimpleQueryFulltext = SimpleQueryFulltext;
exports.SimpleQueryGeo = SimpleQueryGeo;
exports.SimpleQueryNear = SimpleQueryNear;
exports.SimpleQueryRange = SimpleQueryRange;
exports.SimpleQueryWithin = SimpleQueryWithin;
exports.SimpleQueryWithinRectangle = SimpleQueryWithinRectangle;