summaryrefslogtreecommitdiff
path: root/src/mongo/db/query/parsed_projection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/query/parsed_projection.cpp')
-rw-r--r--src/mongo/db/query/parsed_projection.cpp477
1 files changed, 232 insertions, 245 deletions
diff --git a/src/mongo/db/query/parsed_projection.cpp b/src/mongo/db/query/parsed_projection.cpp
index 7552475d5f1..4ebcff2d1a3 100644
--- a/src/mongo/db/query/parsed_projection.cpp
+++ b/src/mongo/db/query/parsed_projection.cpp
@@ -32,296 +32,283 @@
namespace mongo {
- using std::unique_ptr;
- using std::string;
-
- /**
- * Parses the projection 'spec' and checks its validity with respect to the query 'query'.
- * Puts covering information into 'out'.
- *
- * Does not take ownership of 'query'.
- *
- * Returns Status::OK() if it's a valid spec.
- * Returns a Status indicating how it's invalid otherwise.
- */
- // static
- Status ParsedProjection::make(const BSONObj& spec,
- const MatchExpression* const query,
- ParsedProjection** out,
- const MatchExpressionParser::WhereCallback& whereCallback) {
- // Are we including or excluding fields? Values:
- // -1 when we haven't initialized it.
- // 1 when we're including
- // 0 when we're excluding.
- int include_exclude = -1;
-
- // If any of these are 'true' the projection isn't covered.
- bool include = true;
- bool hasNonSimple = false;
- bool hasDottedField = false;
-
- bool includeID = true;
-
- bool hasIndexKeyProjection = false;
-
- bool wantGeoNearPoint = false;
- bool wantGeoNearDistance = false;
-
- // Until we see a positional or elemMatch operator we're normal.
- ArrayOpType arrayOpType = ARRAY_OP_NORMAL;
-
- BSONObjIterator it(spec);
- while (it.more()) {
- BSONElement e = it.next();
-
- if (!e.isNumber() && !e.isBoolean()) {
- hasNonSimple = true;
- }
-
- if (Object == e.type()) {
- BSONObj obj = e.embeddedObject();
- if (1 != obj.nFields()) {
- return Status(ErrorCodes::BadValue, ">1 field in obj: " + obj.toString());
- }
-
- BSONElement e2 = obj.firstElement();
- if (mongoutils::str::equals(e2.fieldName(), "$slice")) {
- if (e2.isNumber()) {
- // This is A-OK.
- }
- else if (e2.type() == Array) {
- BSONObj arr = e2.embeddedObject();
- if (2 != arr.nFields()) {
- return Status(ErrorCodes::BadValue, "$slice array wrong size");
- }
-
- BSONObjIterator it(arr);
- // Skip over 'skip'.
- it.next();
- int limit = it.next().numberInt();
- if (limit <= 0) {
- return Status(ErrorCodes::BadValue, "$slice limit must be positive");
- }
- }
- else {
- return Status(ErrorCodes::BadValue,
- "$slice only supports numbers and [skip, limit] arrays");
- }
- }
- else if (mongoutils::str::equals(e2.fieldName(), "$elemMatch")) {
- // Validate $elemMatch arguments and dependencies.
- if (Object != e2.type()) {
- return Status(ErrorCodes::BadValue,
- "elemMatch: Invalid argument, object required.");
- }
+using std::unique_ptr;
+using std::string;
- if (ARRAY_OP_POSITIONAL == arrayOpType) {
- return Status(ErrorCodes::BadValue,
- "Cannot specify positional operator and $elemMatch.");
- }
-
- if (mongoutils::str::contains(e.fieldName(), '.')) {
- return Status(ErrorCodes::BadValue,
- "Cannot use $elemMatch projection on a nested field.");
- }
-
- arrayOpType = ARRAY_OP_ELEM_MATCH;
-
- // Create a MatchExpression for the elemMatch.
- BSONObj elemMatchObj = e.wrap();
- verify(elemMatchObj.isOwned());
-
- // TODO: Is there a faster way of validating the elemMatchObj?
- StatusWithMatchExpression swme = MatchExpressionParser::parse(elemMatchObj,
- whereCallback);
- if (!swme.isOK()) {
- return swme.getStatus();
- }
- delete swme.getValue();
- }
- else if (mongoutils::str::equals(e2.fieldName(), "$meta")) {
- // Field for meta must be top level. We can relax this at some point.
- if (mongoutils::str::contains(e.fieldName(), '.')) {
- return Status(ErrorCodes::BadValue, "field for $meta cannot be nested");
- }
+/**
+ * Parses the projection 'spec' and checks its validity with respect to the query 'query'.
+ * Puts covering information into 'out'.
+ *
+ * Does not take ownership of 'query'.
+ *
+ * Returns Status::OK() if it's a valid spec.
+ * Returns a Status indicating how it's invalid otherwise.
+ */
+// static
+Status ParsedProjection::make(const BSONObj& spec,
+ const MatchExpression* const query,
+ ParsedProjection** out,
+ const MatchExpressionParser::WhereCallback& whereCallback) {
+ // Are we including or excluding fields? Values:
+ // -1 when we haven't initialized it.
+ // 1 when we're including
+ // 0 when we're excluding.
+ int include_exclude = -1;
+
+ // If any of these are 'true' the projection isn't covered.
+ bool include = true;
+ bool hasNonSimple = false;
+ bool hasDottedField = false;
+
+ bool includeID = true;
+
+ bool hasIndexKeyProjection = false;
+
+ bool wantGeoNearPoint = false;
+ bool wantGeoNearDistance = false;
+
+ // Until we see a positional or elemMatch operator we're normal.
+ ArrayOpType arrayOpType = ARRAY_OP_NORMAL;
+
+ BSONObjIterator it(spec);
+ while (it.more()) {
+ BSONElement e = it.next();
+
+ if (!e.isNumber() && !e.isBoolean()) {
+ hasNonSimple = true;
+ }
- // Make sure the argument to $meta is something we recognize.
- // e.g. {x: {$meta: "textScore"}}
- if (String != e2.type()) {
- return Status(ErrorCodes::BadValue, "unexpected argument to $meta in proj");
- }
+ if (Object == e.type()) {
+ BSONObj obj = e.embeddedObject();
+ if (1 != obj.nFields()) {
+ return Status(ErrorCodes::BadValue, ">1 field in obj: " + obj.toString());
+ }
- if (e2.valuestr() != LiteParsedQuery::metaTextScore
- && e2.valuestr() != LiteParsedQuery::metaRecordId
- && e2.valuestr() != LiteParsedQuery::metaIndexKey
- && e2.valuestr() != LiteParsedQuery::metaGeoNearDistance
- && e2.valuestr() != LiteParsedQuery::metaGeoNearPoint) {
- return Status(ErrorCodes::BadValue,
- "unsupported $meta operator: " + e2.str());
+ BSONElement e2 = obj.firstElement();
+ if (mongoutils::str::equals(e2.fieldName(), "$slice")) {
+ if (e2.isNumber()) {
+ // This is A-OK.
+ } else if (e2.type() == Array) {
+ BSONObj arr = e2.embeddedObject();
+ if (2 != arr.nFields()) {
+ return Status(ErrorCodes::BadValue, "$slice array wrong size");
}
- // This clobbers everything else.
- if (e2.valuestr() == LiteParsedQuery::metaIndexKey) {
- hasIndexKeyProjection = true;
- }
- else if (e2.valuestr() == LiteParsedQuery::metaGeoNearDistance) {
- wantGeoNearDistance = true;
+ BSONObjIterator it(arr);
+ // Skip over 'skip'.
+ it.next();
+ int limit = it.next().numberInt();
+ if (limit <= 0) {
+ return Status(ErrorCodes::BadValue, "$slice limit must be positive");
}
- else if (e2.valuestr() == LiteParsedQuery::metaGeoNearPoint) {
- wantGeoNearPoint = true;
- }
- }
- else {
+ } else {
return Status(ErrorCodes::BadValue,
- string("Unsupported projection option: ") + e.toString());
+ "$slice only supports numbers and [skip, limit] arrays");
}
- }
- else if (mongoutils::str::equals(e.fieldName(), "_id") && !e.trueValue()) {
- includeID = false;
- }
- else {
- // Projections of dotted fields aren't covered.
- if (mongoutils::str::contains(e.fieldName(), '.')) {
- hasDottedField = true;
+ } else if (mongoutils::str::equals(e2.fieldName(), "$elemMatch")) {
+ // Validate $elemMatch arguments and dependencies.
+ if (Object != e2.type()) {
+ return Status(ErrorCodes::BadValue,
+ "elemMatch: Invalid argument, object required.");
}
- // Validate input.
- if (include_exclude == -1) {
- // If we haven't specified an include/exclude, initialize include_exclude.
- // We expect further include/excludes to match it.
- include_exclude = e.trueValue();
- include = !e.trueValue();
- }
- else if (static_cast<bool>(include_exclude) != e.trueValue()) {
- // Make sure that the incl./excl. matches the previous.
+ if (ARRAY_OP_POSITIONAL == arrayOpType) {
return Status(ErrorCodes::BadValue,
- "Projection cannot have a mix of inclusion and exclusion.");
+ "Cannot specify positional operator and $elemMatch.");
}
- }
-
- if (_isPositionalOperator(e.fieldName())) {
- // Validate the positional op.
- if (!e.trueValue()) {
+ if (mongoutils::str::contains(e.fieldName(), '.')) {
return Status(ErrorCodes::BadValue,
- "Cannot exclude array elements with the positional operator.");
+ "Cannot use $elemMatch projection on a nested field.");
}
- if (ARRAY_OP_POSITIONAL == arrayOpType) {
- return Status(ErrorCodes::BadValue,
- "Cannot specify more than one positional proj. per query.");
+ arrayOpType = ARRAY_OP_ELEM_MATCH;
+
+ // Create a MatchExpression for the elemMatch.
+ BSONObj elemMatchObj = e.wrap();
+ verify(elemMatchObj.isOwned());
+
+ // TODO: Is there a faster way of validating the elemMatchObj?
+ StatusWithMatchExpression swme =
+ MatchExpressionParser::parse(elemMatchObj, whereCallback);
+ if (!swme.isOK()) {
+ return swme.getStatus();
+ }
+ delete swme.getValue();
+ } else if (mongoutils::str::equals(e2.fieldName(), "$meta")) {
+ // Field for meta must be top level. We can relax this at some point.
+ if (mongoutils::str::contains(e.fieldName(), '.')) {
+ return Status(ErrorCodes::BadValue, "field for $meta cannot be nested");
}
- if (ARRAY_OP_ELEM_MATCH == arrayOpType) {
- return Status(ErrorCodes::BadValue,
- "Cannot specify positional operator and $elemMatch.");
+ // Make sure the argument to $meta is something we recognize.
+ // e.g. {x: {$meta: "textScore"}}
+ if (String != e2.type()) {
+ return Status(ErrorCodes::BadValue, "unexpected argument to $meta in proj");
}
- std::string after = mongoutils::str::after(e.fieldName(), ".$");
- if (mongoutils::str::contains(after, ".$")) {
- mongoutils::str::stream ss;
- ss << "Positional projection '" << e.fieldName() << "' contains "
- << "the positional operator more than once.";
- return Status(ErrorCodes::BadValue, ss);
+ if (e2.valuestr() != LiteParsedQuery::metaTextScore &&
+ e2.valuestr() != LiteParsedQuery::metaRecordId &&
+ e2.valuestr() != LiteParsedQuery::metaIndexKey &&
+ e2.valuestr() != LiteParsedQuery::metaGeoNearDistance &&
+ e2.valuestr() != LiteParsedQuery::metaGeoNearPoint) {
+ return Status(ErrorCodes::BadValue, "unsupported $meta operator: " + e2.str());
}
- std::string matchfield = mongoutils::str::before(e.fieldName(), '.');
- if (!_hasPositionalOperatorMatch(query, matchfield)) {
- mongoutils::str::stream ss;
- ss << "Positional projection '" << e.fieldName() << "' does not "
- << "match the query document.";
- return Status(ErrorCodes::BadValue, ss);
+ // This clobbers everything else.
+ if (e2.valuestr() == LiteParsedQuery::metaIndexKey) {
+ hasIndexKeyProjection = true;
+ } else if (e2.valuestr() == LiteParsedQuery::metaGeoNearDistance) {
+ wantGeoNearDistance = true;
+ } else if (e2.valuestr() == LiteParsedQuery::metaGeoNearPoint) {
+ wantGeoNearPoint = true;
}
+ } else {
+ return Status(ErrorCodes::BadValue,
+ string("Unsupported projection option: ") + e.toString());
+ }
+ } else if (mongoutils::str::equals(e.fieldName(), "_id") && !e.trueValue()) {
+ includeID = false;
+ } else {
+ // Projections of dotted fields aren't covered.
+ if (mongoutils::str::contains(e.fieldName(), '.')) {
+ hasDottedField = true;
+ }
- arrayOpType = ARRAY_OP_POSITIONAL;
+ // Validate input.
+ if (include_exclude == -1) {
+ // If we haven't specified an include/exclude, initialize include_exclude.
+ // We expect further include/excludes to match it.
+ include_exclude = e.trueValue();
+ include = !e.trueValue();
+ } else if (static_cast<bool>(include_exclude) != e.trueValue()) {
+ // Make sure that the incl./excl. matches the previous.
+ return Status(ErrorCodes::BadValue,
+ "Projection cannot have a mix of inclusion and exclusion.");
}
}
- // Fill out the returned obj.
- unique_ptr<ParsedProjection> pp(new ParsedProjection());
-
- // The positional operator uses the MatchDetails from the query
- // expression to know which array element was matched.
- pp->_requiresMatchDetails = arrayOpType == ARRAY_OP_POSITIONAL;
- // Save the raw spec. It should be owned by the LiteParsedQuery.
- verify(spec.isOwned());
- pp->_source = spec;
- pp->_returnKey = hasIndexKeyProjection;
+ if (_isPositionalOperator(e.fieldName())) {
+ // Validate the positional op.
+ if (!e.trueValue()) {
+ return Status(ErrorCodes::BadValue,
+ "Cannot exclude array elements with the positional operator.");
+ }
- // Dotted fields aren't covered, non-simple require match details, and as for include, "if
- // we default to including then we can't use an index because we don't know what we're
- // missing."
- pp->_requiresDocument = include || hasNonSimple || hasDottedField;
+ if (ARRAY_OP_POSITIONAL == arrayOpType) {
+ return Status(ErrorCodes::BadValue,
+ "Cannot specify more than one positional proj. per query.");
+ }
- // Add geoNear projections.
- pp->_wantGeoNearPoint = wantGeoNearPoint;
- pp->_wantGeoNearDistance = wantGeoNearDistance;
+ if (ARRAY_OP_ELEM_MATCH == arrayOpType) {
+ return Status(ErrorCodes::BadValue,
+ "Cannot specify positional operator and $elemMatch.");
+ }
- // If it's possible to compute the projection in a covered fashion, populate _requiredFields
- // so the planner can perform projection analysis.
- if (!pp->_requiresDocument) {
- if (includeID) {
- pp->_requiredFields.push_back("_id");
+ std::string after = mongoutils::str::after(e.fieldName(), ".$");
+ if (mongoutils::str::contains(after, ".$")) {
+ mongoutils::str::stream ss;
+ ss << "Positional projection '" << e.fieldName() << "' contains "
+ << "the positional operator more than once.";
+ return Status(ErrorCodes::BadValue, ss);
}
- // The only way we could be here is if spec is only simple non-dotted-field projections.
- // Therefore we can iterate over spec to get the fields required.
- BSONObjIterator srcIt(spec);
- while (srcIt.more()) {
- BSONElement elt = srcIt.next();
- // We've already handled the _id field before entering this loop.
- if (includeID && mongoutils::str::equals(elt.fieldName(), "_id")) {
- continue;
- }
- if (elt.trueValue()) {
- pp->_requiredFields.push_back(elt.fieldName());
- }
+ std::string matchfield = mongoutils::str::before(e.fieldName(), '.');
+ if (!_hasPositionalOperatorMatch(query, matchfield)) {
+ mongoutils::str::stream ss;
+ ss << "Positional projection '" << e.fieldName() << "' does not "
+ << "match the query document.";
+ return Status(ErrorCodes::BadValue, ss);
}
+
+ arrayOpType = ARRAY_OP_POSITIONAL;
}
+ }
+
+ // Fill out the returned obj.
+ unique_ptr<ParsedProjection> pp(new ParsedProjection());
+
+ // The positional operator uses the MatchDetails from the query
+ // expression to know which array element was matched.
+ pp->_requiresMatchDetails = arrayOpType == ARRAY_OP_POSITIONAL;
+
+ // Save the raw spec. It should be owned by the LiteParsedQuery.
+ verify(spec.isOwned());
+ pp->_source = spec;
+ pp->_returnKey = hasIndexKeyProjection;
- // returnKey clobbers everything.
- if (hasIndexKeyProjection) {
- pp->_requiresDocument = false;
+ // Dotted fields aren't covered, non-simple require match details, and as for include, "if
+ // we default to including then we can't use an index because we don't know what we're
+ // missing."
+ pp->_requiresDocument = include || hasNonSimple || hasDottedField;
+
+ // Add geoNear projections.
+ pp->_wantGeoNearPoint = wantGeoNearPoint;
+ pp->_wantGeoNearDistance = wantGeoNearDistance;
+
+ // If it's possible to compute the projection in a covered fashion, populate _requiredFields
+ // so the planner can perform projection analysis.
+ if (!pp->_requiresDocument) {
+ if (includeID) {
+ pp->_requiredFields.push_back("_id");
}
- *out = pp.release();
- return Status::OK();
+ // The only way we could be here is if spec is only simple non-dotted-field projections.
+ // Therefore we can iterate over spec to get the fields required.
+ BSONObjIterator srcIt(spec);
+ while (srcIt.more()) {
+ BSONElement elt = srcIt.next();
+ // We've already handled the _id field before entering this loop.
+ if (includeID && mongoutils::str::equals(elt.fieldName(), "_id")) {
+ continue;
+ }
+ if (elt.trueValue()) {
+ pp->_requiredFields.push_back(elt.fieldName());
+ }
+ }
}
- // static
- bool ParsedProjection::_isPositionalOperator(const char* fieldName) {
- return mongoutils::str::contains(fieldName, ".$") &&
- !mongoutils::str::contains(fieldName, ".$ref") &&
- !mongoutils::str::contains(fieldName, ".$id") &&
- !mongoutils::str::contains(fieldName, ".$db");
-
+ // returnKey clobbers everything.
+ if (hasIndexKeyProjection) {
+ pp->_requiresDocument = false;
}
- // static
- bool ParsedProjection::_hasPositionalOperatorMatch(const MatchExpression* const query,
- const std::string& matchfield) {
- if (query->isLogical()) {
- for (unsigned int i = 0; i < query->numChildren(); ++i) {
- if (_hasPositionalOperatorMatch(query->getChild(i), matchfield)) {
- return true;
- }
+ *out = pp.release();
+ return Status::OK();
+}
+
+// static
+bool ParsedProjection::_isPositionalOperator(const char* fieldName) {
+ return mongoutils::str::contains(fieldName, ".$") &&
+ !mongoutils::str::contains(fieldName, ".$ref") &&
+ !mongoutils::str::contains(fieldName, ".$id") &&
+ !mongoutils::str::contains(fieldName, ".$db");
+}
+
+// static
+bool ParsedProjection::_hasPositionalOperatorMatch(const MatchExpression* const query,
+ const std::string& matchfield) {
+ if (query->isLogical()) {
+ for (unsigned int i = 0; i < query->numChildren(); ++i) {
+ if (_hasPositionalOperatorMatch(query->getChild(i), matchfield)) {
+ return true;
}
}
- else {
- StringData queryPath = query->path();
- const char* pathRawData = queryPath.rawData();
- // We have to make a distinction between match expressions that are
- // initialized with an empty field/path name "" and match expressions
- // for which the path is not meaningful (eg. $where and the internal
- // expression type ALWAYS_FALSE).
- if (!pathRawData) {
- return false;
- }
- std::string pathPrefix = mongoutils::str::before(pathRawData, '.');
- return pathPrefix == matchfield;
+ } else {
+ StringData queryPath = query->path();
+ const char* pathRawData = queryPath.rawData();
+ // We have to make a distinction between match expressions that are
+ // initialized with an empty field/path name "" and match expressions
+ // for which the path is not meaningful (eg. $where and the internal
+ // expression type ALWAYS_FALSE).
+ if (!pathRawData) {
+ return false;
}
- return false;
+ std::string pathPrefix = mongoutils::str::before(pathRawData, '.');
+ return pathPrefix == matchfield;
}
+ return false;
+}
} // namespace mongo