diff options
Diffstat (limited to 'src/mongo/db/query/parsed_projection.cpp')
-rw-r--r-- | src/mongo/db/query/parsed_projection.cpp | 477 |
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 |