/** * Copyright (C) 2013 10gen 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 . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the GNU Affero General Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ #include "mongo/db/query/parsed_projection.h" #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/query/query_request.h" 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(OperationContext* opCtx, const BSONObj& spec, const MatchExpression* const query, ParsedProjection** out) { // Whether we're including or excluding fields. enum class IncludeExclude { kUninitialized, kInclude, kExclude }; IncludeExclude includeExclude = IncludeExclude::kUninitialized; bool requiresDocument = false; bool hasIndexKeyProjection = false; bool wantTextScore = false; bool wantGeoNearPoint = false; bool wantGeoNearDistance = false; bool wantSortKey = false; // Until we see a positional or elemMatch operator we're normal. ArrayOpType arrayOpType = ARRAY_OP_NORMAL; // Fill out the returned obj. unique_ptr pp(new ParsedProjection()); pp->_hasId = true; for (auto&& elem : spec) { if (Object == elem.type()) { BSONObj obj = elem.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"); } // Projections with $slice aren't covered. requiresDocument = true; pp->_arrayFields.push_back(elem.fieldNameStringData()); } 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."); } if (ARRAY_OP_POSITIONAL == arrayOpType) { return Status(ErrorCodes::BadValue, "Cannot specify positional operator and $elemMatch."); } if (mongoutils::str::contains(elem.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 = elem.wrap(); invariant(elemMatchObj.isOwned()); // We pass a null pointer instead of threading through the CollatorInterface. This // is ok because the parsed MatchExpression is not used after being created. We are // only parsing here in order to ensure that the elemMatch projection is valid. // // Match expression extensions such as $text, $where, $geoNear, $near, and // $nearSphere are not allowed in $elemMatch projections. $expr and $jsonSchema are // not allowed because the matcher is not applied to the root of the document. const CollatorInterface* collator = nullptr; boost::intrusive_ptr expCtx( new ExpressionContext(opCtx, collator)); StatusWithMatchExpression statusWithMatcher = MatchExpressionParser::parse(elemMatchObj, std::move(expCtx), ExtensionsCallbackNoop(), MatchExpressionParser::kBanAllSpecialFeatures); if (!statusWithMatcher.isOK()) { return statusWithMatcher.getStatus(); } // Projections with $elemMatch aren't covered. requiresDocument = true; pp->_arrayFields.push_back(elem.fieldNameStringData()); } 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(elem.fieldName(), '.')) { return Status(ErrorCodes::BadValue, "field for $meta cannot be nested"); } // 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 (e2.valuestr() != QueryRequest::metaTextScore && e2.valuestr() != QueryRequest::metaRecordId && e2.valuestr() != QueryRequest::metaIndexKey && e2.valuestr() != QueryRequest::metaGeoNearDistance && e2.valuestr() != QueryRequest::metaGeoNearPoint && e2.valuestr() != QueryRequest::metaSortKey) { return Status(ErrorCodes::BadValue, "unsupported $meta operator: " + e2.str()); } // This clobbers everything else. if (e2.valuestr() == QueryRequest::metaTextScore) { wantTextScore = true; } else if (e2.valuestr() == QueryRequest::metaIndexKey) { hasIndexKeyProjection = true; } else if (e2.valuestr() == QueryRequest::metaGeoNearDistance) { wantGeoNearDistance = true; } else if (e2.valuestr() == QueryRequest::metaGeoNearPoint) { wantGeoNearPoint = true; } else if (e2.valuestr() == QueryRequest::metaSortKey) { wantSortKey = true; } // Of the $meta projections, only sortKey can be covered. if (e2.valuestr() != QueryRequest::metaSortKey) { requiresDocument = true; } pp->_metaFields.push_back(elem.fieldNameStringData()); } else { return Status(ErrorCodes::BadValue, string("Unsupported projection option: ") + elem.toString()); } } else if (mongoutils::str::equals(elem.fieldName(), "_id") && !elem.trueValue()) { pp->_hasId = false; } else { pp->_hasDottedFieldPath = pp->_hasDottedFieldPath || elem.fieldNameStringData().find('.') != std::string::npos; if (elem.trueValue()) { pp->_includedFields.push_back(elem.fieldNameStringData()); } else { pp->_excludedFields.push_back(elem.fieldNameStringData()); } // If we haven't specified an include/exclude, initialize includeExclude. We expect // further include/excludes to match it. if (includeExclude == IncludeExclude::kUninitialized) { includeExclude = elem.trueValue() ? IncludeExclude::kInclude : IncludeExclude::kExclude; } else if ((includeExclude == IncludeExclude::kInclude && !elem.trueValue()) || (includeExclude == IncludeExclude::kExclude && elem.trueValue())) { return Status(ErrorCodes::BadValue, "Projection cannot have a mix of inclusion and exclusion."); } } if (_isPositionalOperator(elem.fieldName())) { // Validate the positional op. if (!elem.trueValue()) { return Status(ErrorCodes::BadValue, "Cannot exclude array elements with the positional operator."); } if (ARRAY_OP_POSITIONAL == arrayOpType) { return Status(ErrorCodes::BadValue, "Cannot specify more than one positional proj. per query."); } if (ARRAY_OP_ELEM_MATCH == arrayOpType) { return Status(ErrorCodes::BadValue, "Cannot specify positional operator and $elemMatch."); } std::string after = mongoutils::str::after(elem.fieldName(), ".$"); if (mongoutils::str::contains(after, ".$")) { mongoutils::str::stream ss; ss << "Positional projection '" << elem.fieldName() << "' contains " << "the positional operator more than once."; return Status(ErrorCodes::BadValue, ss); } std::string matchfield = mongoutils::str::before(elem.fieldName(), '.'); if (!_hasPositionalOperatorMatch(query, matchfield)) { mongoutils::str::stream ss; ss << "Positional projection '" << elem.fieldName() << "' does not " << "match the query document."; return Status(ErrorCodes::BadValue, ss); } arrayOpType = ARRAY_OP_POSITIONAL; pp->_arrayFields.push_back(elem.fieldNameStringData()); } } // If includeExclude is uninitialized or set to exclude fields, then we can't use an index // because we don't know what fields we're missing. if (includeExclude == IncludeExclude::kUninitialized || includeExclude == IncludeExclude::kExclude) { requiresDocument = true; } pp->_isInclusionProjection = (includeExclude == IncludeExclude::kInclude); // 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 QueryRequest. verify(spec.isOwned()); pp->_source = spec; pp->_returnKey = hasIndexKeyProjection; pp->_requiresDocument = requiresDocument; // Add meta-projections. pp->_wantTextScore = wantTextScore; pp->_wantGeoNearPoint = wantGeoNearPoint; pp->_wantGeoNearDistance = wantGeoNearDistance; pp->_wantSortKey = wantSortKey; // 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 (pp->_hasId) { pp->_requiredFields.push_back("_id"); } // The only way we could be here is if spec is only simple non-dotted-field inclusions or // the $meta sortKey projection. 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 (pp->_hasId && mongoutils::str::equals(elt.fieldName(), "_id")) { continue; } // $meta sortKey should not be checked as a part of _requiredFields, since it can // potentially produce a covered projection as long as the sort key is covered. if (BSONType::Object == elt.type()) { dassert( SimpleBSONObjComparator::kInstance.evaluate(elt.Obj() == BSON("$meta" << "sortKey"))); continue; } if (elt.trueValue()) { pp->_requiredFields.push_back(elt.fieldName()); } } } // returnKey clobbers everything except for sortKey meta-projection. if (hasIndexKeyProjection && !wantSortKey) { pp->_requiresDocument = false; } *out = pp.release(); return Status::OK(); } namespace { bool isPrefixOf(StringData first, StringData second) { if (first.size() >= second.size()) { return false; } return second.startsWith(first) && second[first.size()] == '.'; } } // namespace bool ParsedProjection::isFieldRetainedExactly(StringData path) const { // If a path, or a parent or child of the path, is contained in _metaFields or in _arrayFields, // our output likely does not preserve that field. for (auto&& metaField : _metaFields) { if (path == metaField || isPrefixOf(path, metaField) || isPrefixOf(metaField, path)) { return false; } } for (auto&& arrayField : _arrayFields) { if (path == arrayField || isPrefixOf(path, arrayField) || isPrefixOf(arrayField, path)) { return false; } } if (path == "_id" || isPrefixOf("_id", path)) { return _hasId; } if (!_isInclusionProjection) { // If we are an exclusion projection, and the path, or a parent or child of the path, is // contained in _excludedFields, our output likely does not preserve that field. for (auto&& excluded : _excludedFields) { if (path == excluded || isPrefixOf(excluded, path) || isPrefixOf(path, excluded)) { return false; } } } else { // If we are an inclusion projection, we may include parents of this path, but we cannot // include children. bool fieldIsIncluded = false; // In a projection with several statements, the last one takes precedence. For example, the // projection {a: 1, a.b: 1} preserves 'a.b', but not 'a'. // TODO SERVER-6527: Simplify this when projections are no longer order-dependent. for (auto&& included : _includedFields) { if (path == included || isPrefixOf(included, path)) { fieldIsIncluded = true; } else if (isPrefixOf(path, included)) { fieldIsIncluded = false; } } if (!fieldIsIncluded) { return false; } } return true; } // 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->getCategory() == MatchExpression::MatchCategory::kLogical) { 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). if (!pathRawData) { return false; } std::string pathPrefix = mongoutils::str::before(pathRawData, '.'); return pathPrefix == matchfield; } return false; } } // namespace mongo