/** * 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/exec/projection_exec.h" #include "mongo/db/exec/working_set_computed_data.h" #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/query/collation/collator_interface.h" #include "mongo/db/query/query_request.h" #include "mongo/util/mongoutils/str.h" namespace mongo { using std::max; using std::string; namespace { /** * Adds sort key metadata inside 'member' to 'builder' with field name 'fieldName'. * * Returns a non-OK status if sort key metadata is missing from 'member'. */ Status addSortKeyMetaProj(StringData fieldName, const WorkingSetMember& member, BSONObjBuilder* builder) { if (!member.hasComputed(WSM_SORT_KEY)) { return Status(ErrorCodes::InternalError, "sortKey meta-projection requested but no data available"); } const SortKeyComputedData* sortKeyData = static_cast(member.getComputed(WSM_SORT_KEY)); builder->append(fieldName, sortKeyData->getSortKey()); return Status::OK(); } } // namespace ProjectionExec::ProjectionExec() : _include(true), _special(false), _includeID(true), _skip(0), _limit(-1), _arrayOpType(ARRAY_OP_NORMAL), _queryExpression(NULL), _hasReturnKey(false) {} ProjectionExec::ProjectionExec(const BSONObj& spec, const MatchExpression* queryExpression, const CollatorInterface* collator, const ExtensionsCallback& extensionsCallback) : _include(true), _special(false), _source(spec), _includeID(true), _skip(0), _limit(-1), _arrayOpType(ARRAY_OP_NORMAL), _queryExpression(queryExpression), _hasReturnKey(false), _collator(collator) { // Whether we're including or excluding fields. enum class IncludeExclude { kUninitialized, kInclude, kExclude }; IncludeExclude includeExclude = IncludeExclude::kUninitialized; BSONObjIterator it(_source); while (it.more()) { BSONElement e = it.next(); if (Object == e.type()) { BSONObj obj = e.embeddedObject(); verify(1 == obj.nFields()); BSONElement e2 = obj.firstElement(); if (mongoutils::str::equals(e2.fieldName(), "$slice")) { if (e2.isNumber()) { int i = e2.numberInt(); if (i < 0) { add(e.fieldName(), i, -i); // limit is now positive } else { add(e.fieldName(), 0, i); } } else { verify(e2.type() == Array); BSONObj arr = e2.embeddedObject(); verify(2 == arr.nFields()); BSONObjIterator it(arr); int skip = it.next().numberInt(); int limit = it.next().numberInt(); verify(limit > 0); add(e.fieldName(), skip, limit); } } else if (mongoutils::str::equals(e2.fieldName(), "$elemMatch")) { _arrayOpType = ARRAY_OP_ELEM_MATCH; // Create a MatchExpression for the elemMatch. BSONObj elemMatchObj = e.wrap(); verify(elemMatchObj.isOwned()); _elemMatchObjs.push_back(elemMatchObj); StatusWithMatchExpression statusWithMatcher = MatchExpressionParser::parse(elemMatchObj, extensionsCallback, _collator); verify(statusWithMatcher.isOK()); // And store it in _matchers. _matchers[mongoutils::str::before(e.fieldName(), '.').c_str()] = statusWithMatcher.getValue().release(); add(e.fieldName(), true); } else if (mongoutils::str::equals(e2.fieldName(), "$meta")) { verify(String == e2.type()); if (e2.valuestr() == QueryRequest::metaTextScore) { _meta[e.fieldName()] = META_TEXT_SCORE; } else if (e2.valuestr() == QueryRequest::metaSortKey) { _sortKeyMetaFields.push_back(e.fieldName()); _meta[_sortKeyMetaFields.back()] = META_SORT_KEY; } else if (e2.valuestr() == QueryRequest::metaRecordId) { _meta[e.fieldName()] = META_RECORDID; } else if (e2.valuestr() == QueryRequest::metaGeoNearPoint) { _meta[e.fieldName()] = META_GEONEAR_POINT; } else if (e2.valuestr() == QueryRequest::metaGeoNearDistance) { _meta[e.fieldName()] = META_GEONEAR_DIST; } else if (e2.valuestr() == QueryRequest::metaIndexKey) { _hasReturnKey = true; } else { // This shouldn't happen, should be caught by parsing. verify(0); } } else { verify(0); } } else if (mongoutils::str::equals(e.fieldName(), "_id") && !e.trueValue()) { _includeID = false; } else { add(e.fieldName(), e.trueValue()); // If we haven't specified an include/exclude, initialize includeExclude. if (includeExclude == IncludeExclude::kUninitialized) { includeExclude = e.trueValue() ? IncludeExclude::kInclude : IncludeExclude::kExclude; _include = !e.trueValue(); } } if (mongoutils::str::contains(e.fieldName(), ".$")) { _arrayOpType = ARRAY_OP_POSITIONAL; } } } ProjectionExec::~ProjectionExec() { for (FieldMap::const_iterator it = _fields.begin(); it != _fields.end(); ++it) { delete it->second; } for (Matchers::const_iterator it = _matchers.begin(); it != _matchers.end(); ++it) { delete it->second; } } void ProjectionExec::add(const string& field, bool include) { if (field.empty()) { // this is the field the user referred to _include = include; } else { _include = !include; const size_t dot = field.find('.'); const string subfield = field.substr(0, dot); const string rest = (dot == string::npos ? "" : field.substr(dot + 1, string::npos)); ProjectionExec*& fm = _fields[subfield.c_str()]; if (NULL == fm) { fm = new ProjectionExec(); } fm->add(rest, include); } } void ProjectionExec::add(const string& field, int skip, int limit) { _special = true; // can't include or exclude whole object if (field.empty()) { // this is the field the user referred to _skip = skip; _limit = limit; } else { const size_t dot = field.find('.'); const string subfield = field.substr(0, dot); const string rest = (dot == string::npos ? "" : field.substr(dot + 1, string::npos)); ProjectionExec*& fm = _fields[subfield.c_str()]; if (NULL == fm) { fm = new ProjectionExec(); } fm->add(rest, skip, limit); } } // // Execution // Status ProjectionExec::transform(WorkingSetMember* member) const { if (_hasReturnKey) { BSONObjBuilder builder; if (member->hasComputed(WSM_INDEX_KEY)) { const IndexKeyComputedData* key = static_cast(member->getComputed(WSM_INDEX_KEY)); builder.appendElements(key->getKey()); } // Must be possible to do both returnKey meta-projection and sortKey meta-projection so that // mongos can support returnKey. for (auto fieldName : _sortKeyMetaFields) { auto sortKeyMetaStatus = addSortKeyMetaProj(fieldName, *member, &builder); if (!sortKeyMetaStatus.isOK()) { return sortKeyMetaStatus; } } member->obj = Snapshotted(SnapshotId(), builder.obj()); member->keyData.clear(); member->recordId = RecordId(); member->transitionToOwnedObj(); return Status::OK(); } BSONObjBuilder bob; if (member->hasObj()) { MatchDetails matchDetails; // If it's a positional projection we need a MatchDetails. if (transformRequiresDetails()) { matchDetails.requestElemMatchKey(); verify(NULL != _queryExpression); verify(_queryExpression->matchesBSON(member->obj.value(), &matchDetails)); // Performing a positional projection requires valid MatchDetails. For example, // ambiguity caused by multiple implicit array traversal predicates can lead to invalid // match details. if (!matchDetails.isValid()) { return Status(ErrorCodes::InternalError, "ambiguous positional projection"); } } Status projStatus = transform(member->obj.value(), &bob, &matchDetails); if (!projStatus.isOK()) { return projStatus; } } else { invariant(!_include); // Go field by field. if (_includeID) { BSONElement elt; // Sometimes the _id field doesn't exist... if (member->getFieldDotted("_id", &elt) && !elt.eoo()) { bob.appendAs(elt, "_id"); } } BSONObjIterator it(_source); while (it.more()) { BSONElement specElt = it.next(); if (mongoutils::str::equals("_id", specElt.fieldName())) { continue; } // $meta sortKey is the only meta-projection which is allowed to operate on index keys // rather than the full document. auto metaIt = _meta.find(specElt.fieldName()); if (metaIt != _meta.end()) { invariant(metaIt->second == META_SORT_KEY); continue; } // $meta sortKey is also the only element with an Object value in the projection spec // that can operate on index keys rather than the full document. invariant(BSONType::Object != specElt.type()); BSONElement keyElt; // We can project a field that doesn't exist. We just ignore it. if (member->getFieldDotted(specElt.fieldName(), &keyElt) && !keyElt.eoo()) { bob.appendAs(keyElt, specElt.fieldName()); } } } for (MetaMap::const_iterator it = _meta.begin(); it != _meta.end(); ++it) { if (META_GEONEAR_DIST == it->second) { if (member->hasComputed(WSM_COMPUTED_GEO_DISTANCE)) { const GeoDistanceComputedData* dist = static_cast( member->getComputed(WSM_COMPUTED_GEO_DISTANCE)); bob.append(it->first, dist->getDist()); } else { return Status(ErrorCodes::InternalError, "near loc dist requested but no data available"); } } else if (META_GEONEAR_POINT == it->second) { if (member->hasComputed(WSM_GEO_NEAR_POINT)) { const GeoNearPointComputedData* point = static_cast( member->getComputed(WSM_GEO_NEAR_POINT)); BSONObj ptObj = point->getPoint(); if (ptObj.couldBeArray()) { bob.appendArray(it->first, ptObj); } else { bob.append(it->first, ptObj); } } else { return Status(ErrorCodes::InternalError, "near loc proj requested but no data available"); } } else if (META_TEXT_SCORE == it->second) { if (member->hasComputed(WSM_COMPUTED_TEXT_SCORE)) { const TextScoreComputedData* score = static_cast( member->getComputed(WSM_COMPUTED_TEXT_SCORE)); bob.append(it->first, score->getScore()); } else { bob.append(it->first, 0.0); } } else if (META_SORT_KEY == it->second) { auto sortKeyMetaStatus = addSortKeyMetaProj(it->first, *member, &bob); if (!sortKeyMetaStatus.isOK()) { return sortKeyMetaStatus; } } else if (META_RECORDID == it->second) { bob.append(it->first, static_cast(member->recordId.repr())); } } BSONObj newObj = bob.obj(); member->obj = Snapshotted(SnapshotId(), newObj); member->keyData.clear(); member->recordId = RecordId(); member->transitionToOwnedObj(); return Status::OK(); } Status ProjectionExec::transform(const BSONObj& in, BSONObjBuilder* bob, const MatchDetails* details) const { const ArrayOpType& arrayOpType = _arrayOpType; BSONObjIterator it(in); while (it.more()) { BSONElement elt = it.next(); // Case 1: _id if (mongoutils::str::equals("_id", elt.fieldName())) { if (_includeID) { bob->append(elt); } continue; } // Case 2: no array projection for this field. Matchers::const_iterator matcher = _matchers.find(elt.fieldName()); if (_matchers.end() == matcher) { Status s = append(bob, elt, details, arrayOpType); if (!s.isOK()) { return s; } continue; } // Case 3: field has array projection with $elemMatch specified. if (ARRAY_OP_ELEM_MATCH != arrayOpType) { return Status(ErrorCodes::BadValue, "Matchers are only supported for $elemMatch"); } MatchDetails arrayDetails; arrayDetails.requestElemMatchKey(); if (matcher->second->matchesBSON(in, &arrayDetails)) { // Since we create a special matcher for each $elemMatch projection, we should always // have valid MatchDetails. invariant(arrayDetails.isValid()); FieldMap::const_iterator fieldIt = _fields.find(elt.fieldName()); if (_fields.end() == fieldIt) { return Status(ErrorCodes::BadValue, "$elemMatch specified, but projection field not found."); } BSONArrayBuilder arrBuilder; BSONObjBuilder subBob; if (in.getField(elt.fieldName()).eoo()) { return Status(ErrorCodes::InternalError, "$elemMatch called on document element with eoo"); } if (in.getField(elt.fieldName()).Obj().getField(arrayDetails.elemMatchKey()).eoo()) { return Status(ErrorCodes::InternalError, "$elemMatch called on array element with eoo"); } arrBuilder.append( in.getField(elt.fieldName()).Obj().getField(arrayDetails.elemMatchKey())); subBob.appendArray(matcher->first, arrBuilder.arr()); Status status = append(bob, subBob.done().firstElement(), details, arrayOpType); if (!status.isOK()) { return status; } } } return Status::OK(); } void ProjectionExec::appendArray(BSONObjBuilder* bob, const BSONObj& array, bool nested) const { int skip = nested ? 0 : _skip; int limit = nested ? -1 : _limit; if (skip < 0) { skip = max(0, skip + array.nFields()); } int index = 0; BSONObjIterator it(array); while (it.more()) { BSONElement elt = it.next(); if (skip) { skip--; continue; } if (limit != -1 && (limit-- == 0)) { break; } switch (elt.type()) { case Array: { BSONObjBuilder subBob; appendArray(&subBob, elt.embeddedObject(), true); bob->appendArray(bob->numStr(index++), subBob.obj()); break; } case Object: { BSONObjBuilder subBob; BSONObjIterator jt(elt.embeddedObject()); while (jt.more()) { append(&subBob, jt.next()); } bob->append(bob->numStr(index++), subBob.obj()); break; } default: if (_include) { bob->appendAs(elt, bob->numStr(index++)); } } } } Status ProjectionExec::append(BSONObjBuilder* bob, const BSONElement& elt, const MatchDetails* details, const ArrayOpType arrayOpType) const { // Skip if the field name matches a computed $meta field. // $meta projection fields can exist at the top level of // the result document and the field names cannot be dotted. if (_meta.find(elt.fieldName()) != _meta.end()) { return Status::OK(); } FieldMap::const_iterator field = _fields.find(elt.fieldName()); if (field == _fields.end()) { if (_include) { bob->append(elt); } return Status::OK(); } ProjectionExec& subfm = *field->second; if ((subfm._fields.empty() && !subfm._special) || !(elt.type() == Object || elt.type() == Array)) { // field map empty, or element is not an array/object if (subfm._include) { bob->append(elt); } } else if (elt.type() == Object) { BSONObjBuilder subBob; BSONObjIterator it(elt.embeddedObject()); while (it.more()) { subfm.append(&subBob, it.next(), details, arrayOpType); } bob->append(elt.fieldName(), subBob.obj()); } else { // Array BSONObjBuilder matchedBuilder; if (details && arrayOpType == ARRAY_OP_POSITIONAL) { // $ positional operator specified if (!details->hasElemMatchKey()) { mongoutils::str::stream error; error << "positional operator (" << elt.fieldName() << ".$) requires corresponding field" << " in query specifier"; return Status(ErrorCodes::BadValue, error); } if (elt.embeddedObject()[details->elemMatchKey()].eoo()) { return Status(ErrorCodes::BadValue, "positional operator element mismatch"); } // append as the first and only element in the projected array matchedBuilder.appendAs(elt.embeddedObject()[details->elemMatchKey()], "0"); } else { // append exact array; no subarray matcher specified subfm.appendArray(&matchedBuilder, elt.embeddedObject()); } bob->appendArray(elt.fieldName(), matchedBuilder.obj()); } return Status::OK(); } } // namespace mongo