Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 0 additions & 75 deletions jstests/geo_mindistance.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,81 +213,6 @@ assert.eq(
+ cmdResult.results.length
);

//
// Test $minDistance input validation for $near and $nearSphere queries,
// and for geoNear command.
//

/** Some bad inputs for $near and $nearSphere. */
var badMinDistance, badMinDistances = [-1, undefined, 'foo'];
for (var i = 0; i < badMinDistances.length; i++) {
badMinDistance = badMinDistances[i];

assert.throws(
function(minDistance) {
t.find({loc: {$nearSphere: geoJSONPoint, $minDistance: minDistance}}).next();
},
[badMinDistance],
"$nearSphere with GeoJSON point should've failed with $minDistance = " + badMinDistance);

assert.throws(
function(minDistance) {
t.find({loc: {$nearSphere: legacyPoint, $minDistance: minDistance}}).next();
},
[badMinDistance],
"$nearSphere with legacy coordinates should've failed with $minDistance = " + badMinDistance);

assert.throws(
function(minDistance) {
t.find({loc: {$near: geoJSONPoint, $minDistance: minDistance}}).next();
},
[badMinDistance],
"$near with GeoJSON point should've failed with $minDistance = " + badMinDistance);

assert.commandFailed(
db.runCommand({
geoNear: t.getName(),
near: legacyPoint,
minDistance: badMinDistance,
spherical: true
}),
"geoNear with legacy coordinates should've failed with $minDistance = " + badMinDistance);

assert.commandFailed(
db.runCommand({
geoNear: t.getName(),
near: {type: 'Point', coordinates: [0, 0]},
minDistance: badMinDistance,
spherical: true
}),
"geoNear with GeoJSON point should've failed with $minDistance = " + badMinDistance);
}

/* Can't be more than half earth radius in meters. */
badMinDistance = Math.PI * earthRadiusMeters + 10;
assert.throws(
function(minDistance) {
t.find({loc: {$near: geoJSONPoint, $minDistance: minDistance}}).next();
},
[badMinDistance],
"$near should've failed with $minDistance = " + badMinDistance);

assert.throws(
function(minDistance) {
t.find({loc: {$nearSphere: geoJSONPoint, $minDistance: minDistance}}).next();
},
[badMinDistance],
"$nearSphere should've failed with $minDistance = " + badMinDistance);

/* Can't be more than pi. */
badMinDistance = Math.PI + 0.1;
assert.throws(
function(minDistance) {
t.find({loc: {$nearSphere: legacyPoint, $minDistance: minDistance}}).next();
},
[badMinDistance],
"$near should've failed with $minDistance = " + badMinDistance);

//
// Verify that we throw errors using 2d index with $minDistance.
// ($minDistance requires a 2dsphere index, not supported with 2d.)
Expand Down
107 changes: 107 additions & 0 deletions jstests/geo_query_input_validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// Test input validation for $near and $nearSphere queries.
//
var t = db.geo_query_input_validation;

// The test matrix. Some combinations are not supported:
// 2d index and $minDistance.
// 2dsphere index, $near, and legacy coordinates.
var indexTypes = ['2d', '2dsphere'],
queryTypes = ['$near', '$nearSphere'],
pointTypes = [
{$geometry: {type: 'Point', coordinates: [0, 0]}},
[0, 0]],
optionNames = ['$minDistance', '$maxDistance'],
badDistances = [-1, undefined, 'foo'];

indexTypes.forEach(function(indexType) {
t.drop();
t.createIndex({'loc': indexType});

queryTypes.forEach(function(queryType) {
pointTypes.forEach(function(pointType) {
optionNames.forEach(function(optionName) {
var isLegacy = Array.isArray(pointType),
pointDescription = (isLegacy ? "legacy coordinates" : "GeoJSON point");

// Like {loc: {$nearSphere: [0, 0], $maxDistance: 1}}.
var query = {};
query[queryType] = pointType;
query[optionName] = 1;

var locQuery = {loc: query};

if (indexType == '2d' && !isLegacy) {
// Currently doesn't throw errors but also doesn't work as
// expected: SERVER-10636. Stop processing this combination.
return;
}

// Unsupported combinations should return errors.
if (
(indexType == '2d' && optionName == '$minDistance') ||
(indexType == '2dsphere' && queryType == '$near' && isLegacy)
) {
assert.throws(
function() {
t.find(locQuery).itcount();
},
[],
queryType + " query with " + indexType
+ " index and " + pointDescription
+ " should've failed."
);

// Stop processing this combination in the test matrix.
return;
}

// This is a supported combination. No error.
t.find(locQuery).itcount();

function makeQuery(distance) {
// Like {$nearSphere: geoJSONPoint, $minDistance: -1}.
var query = {};
query[queryType] = pointType;
query[optionName] = distance;
return query;
}

function doQuery(query) {
t.find({loc: query}).itcount();
}

// No error with $min/$maxDistance 1.
doQuery(makeQuery(1));

var outOfRangeDistances = [];
if (indexType == '2d' && queryType == '$near') {
// $maxDistance unlimited; no error.
doQuery(makeQuery(1e10));
} else if (isLegacy) {
// Radians can't be more than pi.
outOfRangeDistances.push(Math.PI + 0.1);
} else {
// $min/$maxDistance is in meters, so distances greater
// than pi are ok, but not more than half earth's
// circumference in meters.
doQuery(makeQuery(Math.PI + 0.1));

var earthRadiusMeters = 6378.1 * 1000;
outOfRangeDistances.push(Math.PI * earthRadiusMeters + 100);
}

// Try several bad values for $min/$maxDistance.
badDistances.concat(outOfRangeDistances).forEach(function(badDistance) {

var msg = (
queryType + " with " + pointDescription
+ " and " + indexType + " index should've failed with "
+ optionName + " " + badDistance);

assert.throws(doQuery, [makeQuery(badDistance)], msg);
});
});
});
});
});
2 changes: 1 addition & 1 deletion jstests/geo_s2near.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ assert.throws(function() { return db.runCommand({geoNear : t.getName(), near: so

// Do some basic near searches.
res = t.find({ "geo" : { "$near" : { "$geometry" : origin, $maxDistance: 2000} } }).limit(10)
resNear = db.runCommand({geoNear : t.getName(), near: [0,0], num: 10, maxDistance: 2000, spherical: true})
resNear = db.runCommand({geoNear : t.getName(), near: [0,0], num: 10, maxDistance: Math.PI, spherical: true})
assert.eq(res.itcount(), resNear.results.length, 10)

res = t.find({ "geo" : { "$near" : { "$geometry" : origin } } }).limit(10)
Expand Down
126 changes: 126 additions & 0 deletions jstests/geonear_cmd_input_validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// Test input validation for geoNear command.
//
var t = db.geonear_cmd_input_validation;
t.drop();
t.ensureIndex({loc: "2dsphere"});

// The test matrix. Some combinations are not supported:
// 2d index and minDistance.
// 2d index and GeoJSON
// 2dsphere index and spherical=false
var indexTypes = ['2d', '2dsphere'],
pointTypes = [
{type: 'Point', coordinates: [0, 0]},
[0, 0]],
sphericalOptions = [true, false],
optionNames = ['minDistance', 'maxDistance'],
badNumbers = [-1, undefined, 'foo'];

indexTypes.forEach(function(indexType) {
t.drop();
t.createIndex({'loc': indexType});

pointTypes.forEach(function(pointType) {
sphericalOptions.forEach(function(spherical) {
optionNames.forEach(function(optionName) {
var isLegacy = Array.isArray(pointType),
pointDescription = (isLegacy ? "legacy coordinates" : "GeoJSON point");

function makeCommand(distance) {
var command = {
geoNear: t.getName(),
near: pointType,
spherical: spherical
};
command[optionName] = distance;
return command;
}

// Unsupported combinations should return errors.
if (
(indexType == '2d' && optionName == 'minDistance') ||
(indexType == '2d' && !isLegacy) ||
(indexType == '2dsphere' && !spherical)
) {
assert.commandFailed(
db.runCommand(makeCommand(1)),
"geoNear with spherical=" + spherical + " and " + indexType
+ " index and " + pointDescription
+ " should've failed."
);

// Stop processing this combination in the test matrix.
return;
}

// This is a supported combination. No error.
assert.commandWorked(db.runCommand({
geoNear: t.getName(),
near: pointType,
spherical: spherical
}));

// No error with min/maxDistance 1.
db.runCommand(makeCommand(1));

var outOfRangeDistances = [];
if (indexType == '2d') {
// maxDistance unlimited; no error.
db.runCommand(makeCommand(1e10));
} else if (isLegacy) {
// Radians can't be more than pi.
outOfRangeDistances.push(Math.PI + 0.1);
} else {
// Meters can't be more than half circumference.
var earthRadiusMeters = 6378.1 * 1000;
outOfRangeDistances.push(Math.PI * earthRadiusMeters + 100);
}

// Try several bad values for min/maxDistance.
badNumbers.concat(outOfRangeDistances).forEach(function(badDistance) {

var msg = (
"geoNear with spherical=" + spherical + " and "
+ pointDescription + " and " + indexType
+ " index should've failed with "
+ optionName + " " + badDistance);

assert.commandFailed(
db.runCommand(makeCommand(badDistance)),
msg);
});

// Bad values for limit / num.
['num', 'limit'].forEach(function(limitOptionName) {
badNumbers.forEach(function(badLimit) {

var msg = (
"geoNear with spherical=" + spherical + " and "
+ pointDescription + " and " + indexType
+ " index should've failed with '"
+ limitOptionName + "' " + badLimit);

var command = makeCommand(1);
command[limitOptionName] = badLimit;
assert.commandFailed(db.runCommand(command), msg);
});
});

// Bad values for distanceMultiplier.
badNumbers.forEach(function(badNumber) {

var msg = (
"geoNear with spherical=" + spherical + " and "
+ pointDescription + " and " + indexType
+ " index should've failed with distanceMultiplier "
+ badNumber);

var command = makeCommand(1);
command['distanceMultiplier'] = badNumber;
assert.commandFailed(db.runCommand(command), msg);
});
});
});
});
});
24 changes: 24 additions & 0 deletions src/mongo/db/geo/geoconstants.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (C) 2013 MongoDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

namespace mongo {

// Thanks, Wikipedia.
const double kRadiusOfEarthInMeters = (6378.1 * 1000);

} // namespace mongo
17 changes: 12 additions & 5 deletions src/mongo/db/geo/geonear.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@
namespace mongo {

GeoNearArguments::GeoNearArguments(const BSONObj &cmdObj) {
const char* limitName = cmdObj["num"].isNumber() ? "num" : "limit";
if (cmdObj[limitName].isNumber()) {
numWanted = cmdObj[limitName].numberInt();
// If 'num' is passed, use it and ignore 'limit'. Otherwise use 'limit'.
const char* limitName = !cmdObj["num"].eoo() ? "num" : "limit";
BSONElement eNumWanted = cmdObj[limitName];
if (!eNumWanted.eoo()) {
uassert(17032, str::stream() << limitName << " must be a number",
eNumWanted.isNumber());
numWanted = eNumWanted.numberInt();
} else {
numWanted = 100;
}
Expand All @@ -61,8 +65,11 @@ namespace mongo {
query = cmdObj["query"].embeddedObject();
}

if (cmdObj["distanceMultiplier"].isNumber()) {
distanceMultiplier = cmdObj["distanceMultiplier"].number();
BSONElement eDistanceMultiplier = cmdObj["distanceMultiplier"];
if (!eDistanceMultiplier.eoo()) {
uassert(17033, "distanceMultiplier must be a number", eDistanceMultiplier.isNumber());
distanceMultiplier = eDistanceMultiplier.number();
uassert(17034, "distanceMultiplier must be non-negative", distanceMultiplier >= 0);
} else {
distanceMultiplier = 1.0;
}
Expand Down
Loading