diff options
Diffstat (limited to 'src/mongo/db/exec/projection_exec.cpp')
| -rw-r--r-- | src/mongo/db/exec/projection_exec.cpp | 551 |
1 files changed, 551 insertions, 0 deletions
diff --git a/src/mongo/db/exec/projection_exec.cpp b/src/mongo/db/exec/projection_exec.cpp new file mode 100644 index 00000000000..27eed3f90d4 --- /dev/null +++ b/src/mongo/db/exec/projection_exec.cpp @@ -0,0 +1,551 @@ +/** + * 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 <http://www.gnu.org/licenses/>. + * + * 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_parser.h" +#include "mongo/db/matcher/expression.h" +#include "mongo/db/query/lite_parsed_query.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { + + ProjectionExec::ProjectionExec() + : _include(true), + _special(false), + _includeID(true), + _skip(0), + _limit(-1), + _arrayOpType(ARRAY_OP_NORMAL), + _hasNonSimple(false), + _hasDottedField(false), + _queryExpression(NULL), + _hasReturnKey(false) { } + + + ProjectionExec::ProjectionExec(const BSONObj& spec, const MatchExpression* queryExpression) + : _include(true), + _special(false), + _source(spec), + _includeID(true), + _skip(0), + _limit(-1), + _arrayOpType(ARRAY_OP_NORMAL), + _hasNonSimple(false), + _hasDottedField(false), + _queryExpression(queryExpression), + _hasReturnKey(false) { + + // Are we including or excluding fields? + // -1 when we haven't initialized it. + // 1 when we're including + // 0 when we're excluding. + int include_exclude = -1; + + BSONObjIterator it(_source); + while (it.more()) { + BSONElement e = it.next(); + + if (!e.isNumber() && !e.isBoolean()) { + _hasNonSimple = true; + } + + 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 swme = MatchExpressionParser::parse(elemMatchObj); + verify(swme.isOK()); + // And store it in _matchers. + _matchers[mongoutils::str::before(e.fieldName(), '.').c_str()] + = swme.getValue(); + + add(e.fieldName(), true); + } + else if (mongoutils::str::equals(e2.fieldName(), "$meta")) { + verify(String == e2.type()); + if (e2.valuestr() == LiteParsedQuery::metaTextScore) { + _meta[e.fieldName()] = META_TEXT_SCORE; + } + else if (e2.valuestr() == LiteParsedQuery::metaDiskLoc) { + _meta[e.fieldName()] = META_DISKLOC; + } + else if (e2.valuestr() == LiteParsedQuery::metaGeoNearPoint) { + _meta[e.fieldName()] = META_GEONEAR_POINT; + } + else if (e2.valuestr() == LiteParsedQuery::metaGeoNearDistance) { + _meta[e.fieldName()] = META_GEONEAR_DIST; + } + else if (e2.valuestr() == LiteParsedQuery::metaIndexKey) { + _hasReturnKey = true; + // The index key clobbers everything so just stop parsing here. + return; + } + 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()); + + // Projections of dotted fields aren't covered. + if (mongoutils::str::contains(e.fieldName(), '.')) { + _hasDottedField = true; + } + + // 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(); + } + } + + 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) { + BSONObj keyObj; + + if (member->hasComputed(WSM_INDEX_KEY)) { + const IndexKeyComputedData* key + = static_cast<const IndexKeyComputedData*>(member->getComputed(WSM_INDEX_KEY)); + keyObj = key->getKey(); + } + + member->state = WorkingSetMember::OWNED_OBJ; + member->obj = keyObj; + member->keyData.clear(); + member->loc = DiskLoc(); + 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, &matchDetails)); + } + + Status projStatus = transform(member->obj, &bob, &matchDetails); + if (!projStatus.isOK()) { + return projStatus; + } + } + else { + verify(!requiresDocument()); + // 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; + } + + 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<const GeoDistanceComputedData*>( + 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<const GeoNearPointComputedData*>( + 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<const TextScoreComputedData*>( + member->getComputed(WSM_COMPUTED_TEXT_SCORE)); + bob.append(it->first, score->getScore()); + } + else { + bob.append(it->first, 0.0); + } + } + else if (META_DISKLOC == it->second) { + bob.append(it->first, member->loc.toBSONObj()); + } + } + + BSONObj newObj = bob.obj(); + member->state = WorkingSetMember::OWNED_OBJ; + member->obj = newObj; + member->keyData.clear(); + member->loc = DiskLoc(); + + return Status::OK(); + } + + Status ProjectionExec::transform(const BSONObj& in, BSONObj* out) const { + // If it's a positional projection we need a MatchDetails. + MatchDetails matchDetails; + if (transformRequiresDetails()) { + matchDetails.requestElemMatchKey(); + verify(NULL != _queryExpression); + verify(_queryExpression->matchesBSON(in, &matchDetails)); + } + + BSONObjBuilder bob; + Status s = transform(in, &bob, &matchDetails); + if (!s.isOK()) { + return s; + } + *out = bob.obj(); + 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)) { + 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 |
