diff options
author | Ian Boros <ian.boros@mongodb.com> | 2019-10-23 22:04:36 +0000 |
---|---|---|
committer | evergreen <evergreen@mongodb.com> | 2019-10-23 22:04:36 +0000 |
commit | fcff61405cffec61340caf4090615317b5ac5991 (patch) | |
tree | 4f98476c83b72b8ac4273bd2a03bd747bd0e7881 /src/mongo/db/exec | |
parent | ec1cad447dffe1ffcce14ab03e268222dfd32d01 (diff) | |
download | mongo-fcff61405cffec61340caf4090615317b5ac5991.tar.gz |
SERVER-42435 Remove ParsedProjection and ProjectionExec
Diffstat (limited to 'src/mongo/db/exec')
-rw-r--r-- | src/mongo/db/exec/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/exec/projection.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_exec.cpp | 489 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_exec.h | 261 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_exec_test.cpp | 310 | ||||
-rw-r--r-- | src/mongo/db/exec/projection_executor_test.cpp | 53 |
6 files changed, 54 insertions, 1064 deletions
diff --git a/src/mongo/db/exec/SConscript b/src/mongo/db/exec/SConscript index cadce17d459..d8c2926a43d 100644 --- a/src/mongo/db/exec/SConscript +++ b/src/mongo/db/exec/SConscript @@ -79,7 +79,6 @@ env.CppUnitTest( "document_value/value_comparator_test.cpp", "find_projection_executor_test.cpp", "projection_exec_agg_test.cpp", - "projection_exec_test.cpp", "projection_executor_test.cpp", "queued_data_stage_test.cpp", "sort_test.cpp", diff --git a/src/mongo/db/exec/projection.cpp b/src/mongo/db/exec/projection.cpp index 5d50aee547c..d824df21a6c 100644 --- a/src/mongo/db/exec/projection.cpp +++ b/src/mongo/db/exec/projection.cpp @@ -127,8 +127,8 @@ void ProjectionStage::getSimpleInclusionFields(const BSONObj& projObj, FieldSet* // The _id is included by default. bool includeId = true; - // Figure out what fields are in the projection. TODO: we can get this from the - // ParsedProjection...modify that to have this type instead of a vector. + // Figure out what fields are in the projection. We could eventually do this using the + // Projection AST. BSONObjIterator projObjIt(projObj); while (projObjIt.more()) { BSONElement elt = projObjIt.next(); diff --git a/src/mongo/db/exec/projection_exec.cpp b/src/mongo/db/exec/projection_exec.cpp deleted file mode 100644 index bf212a0b44b..00000000000 --- a/src/mongo/db/exec/projection_exec.cpp +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * 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 - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * 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 Server Side 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/bson/mutable/document.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/db/update/path_support.h" -#include "mongo/util/decimal_counter.h" -#include "mongo/util/str.h" - -namespace mongo { - -using std::string; - -namespace mmb = mongo::mutablebson; - -ProjectionExec::ProjectionExec(OperationContext* opCtx, - const BSONObj& spec, - const MatchExpression* queryExpression, - const CollatorInterface* collator) - : _source(spec), _queryExpression(queryExpression), _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(); - invariant(1 == obj.nFields()); - - BSONElement e2 = obj.firstElement(); - if (e2.fieldNameStringData() == "$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 { - invariant(e2.type() == Array); - BSONObj arr = e2.embeddedObject(); - invariant(2 == arr.nFields()); - - BSONObjIterator it(arr); - int skip = it.next().numberInt(); - int limit = it.next().numberInt(); - - invariant(limit > 0); - - add(e.fieldName(), skip, limit); - } - } else if (e2.fieldNameStringData() == "$elemMatch") { - _arrayOpType = ARRAY_OP_ELEM_MATCH; - - // Create a MatchExpression for the elemMatch. - BSONObj elemMatchObj = e.wrap(); - invariant(elemMatchObj.isOwned()); - _elemMatchObjs.push_back(elemMatchObj); - boost::intrusive_ptr<ExpressionContext> expCtx( - new ExpressionContext(opCtx, _collator)); - StatusWithMatchExpression statusWithMatcher = - MatchExpressionParser::parse(elemMatchObj, std::move(expCtx)); - invariant(statusWithMatcher.isOK()); - // And store it in _matchers. - _matchers[str::before(e.fieldNameStringData(), '.')] = - statusWithMatcher.getValue().release(); - - add(e.fieldName(), true); - } else if (e2.fieldNameStringData() == "$meta") { - invariant(String == e2.type()); - if (e2.valuestr() == QueryRequest::metaTextScore) { - _meta[e.fieldName()] = META_TEXT_SCORE; - _needsTextScore = true; - } else if (e2.valuestr() == QueryRequest::metaSortKey) { - _meta[e.fieldName()] = META_SORT_KEY; - _needsSortKey = true; - } else if (e2.valuestr() == QueryRequest::metaRecordId) { - _meta[e.fieldName()] = META_RECORDID; - } else if (e2.valuestr() == QueryRequest::metaGeoNearPoint) { - _meta[e.fieldName()] = META_GEONEAR_POINT; - _needsGeoNearPoint = true; - } else if (e2.valuestr() == QueryRequest::metaGeoNearDistance) { - _meta[e.fieldName()] = META_GEONEAR_DIST; - _needsGeoNearDistance = true; - } else { - // This shouldn't happen, should be caught by parsing. - MONGO_UNREACHABLE; - } - } else { - MONGO_UNREACHABLE; - } - } else if ((e.fieldNameStringData() == "_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 (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 (nullptr == 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 (nullptr == fm) { - fm = new ProjectionExec(); - } - - fm->add(rest, skip, limit); - } -} - -// -// Execution -// - -StatusWith<BSONObj> ProjectionExec::project(const BSONObj& in, - const boost::optional<const double> geoDistance, - Value geoNearPoint, - const BSONObj& sortKey, - const boost::optional<const double> textScore, - const int64_t recordId) const { - BSONObjBuilder bob; - MatchDetails matchDetails; - - // If it's a positional projection we need a MatchDetails. - if (projectRequiresQueryExpression()) { - matchDetails.requestElemMatchKey(); - invariant(nullptr != _queryExpression); - invariant(_queryExpression->matchesBSON(in, &matchDetails)); - } - - Status projStatus = projectHelper(in, &bob, &matchDetails); - if (!projStatus.isOK()) - return projStatus; - else - return {addMeta(std::move(bob), geoDistance, geoNearPoint, sortKey, textScore, recordId)}; -} - -StatusWith<BSONObj> ProjectionExec::projectCovered(const std::vector<IndexKeyDatum>& keyData, - const boost::optional<const double> geoDistance, - Value geoNearPoint, - const BSONObj& sortKey, - const boost::optional<const double> textScore, - const int64_t recordId) const { - invariant(!_include); - BSONObjBuilder bob; - // Go field by field. - if (_includeID) { - boost::optional<BSONElement> elt; - // Sometimes the _id field doesn't exist... - if ((elt = IndexKeyDatum::getFieldDotted(keyData, "_id")) && !elt->eoo()) { - bob.appendAs(elt.get(), "_id"); - } - } - - mmb::Document projectedDoc; - - for (auto&& specElt : _source) { - if (specElt.fieldNameStringData() == "_id") { - 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()); - - boost::optional<BSONElement> keyElt; - // We can project a field that doesn't exist. We just ignore it. - if ((keyElt = IndexKeyDatum::getFieldDotted(keyData, specElt.fieldName())) && - !keyElt->eoo()) { - FieldRef projectedFieldPath{specElt.fieldNameStringData()}; - auto setElementStatus = - pathsupport::setElementAtPath(projectedFieldPath, keyElt.get(), &projectedDoc); - if (!setElementStatus.isOK()) { - return setElementStatus; - } - } - } - - bob.appendElements(projectedDoc.getObject()); - return {addMeta(std::move(bob), geoDistance, geoNearPoint, sortKey, textScore, recordId)}; -} - -BSONObj ProjectionExec::addMeta(BSONObjBuilder bob, - const boost::optional<const double> geoDistance, - Value geoNearPoint, - const BSONObj& sortKey, - const boost::optional<const double> textScore, - const int64_t recordId) const { - for (MetaMap::const_iterator it = _meta.begin(); it != _meta.end(); ++it) { - switch (it->second) { - case META_GEONEAR_DIST: - invariant(geoDistance); - bob.append(it->first, geoDistance.get()); - break; - case META_GEONEAR_POINT: { - invariant(!geoNearPoint.missing()); - geoNearPoint.addToBsonObj(&bob, it->first); - break; - } - case META_TEXT_SCORE: - invariant(textScore); - bob.append(it->first, textScore.get()); - break; - case META_SORT_KEY: { - invariant(!sortKey.isEmpty()); - bob.append(it->first, sortKey); - break; - } - case META_RECORDID: - invariant(recordId != 0); - bob.append(it->first, recordId); - } - } - return bob.obj(); -} - -Status ProjectionExec::projectHelper(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 ("_id" == elt.fieldNameStringData()) { - 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 = std::max(0, skip + array.nFields()); - } - - DecimalCounter<size_t> index; - 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(StringData{index}, subBob.obj()); - ++index; - break; - } - case Object: { - BSONObjBuilder subBob; - BSONObjIterator jt(elt.embeddedObject()); - while (jt.more()) { - append(&subBob, jt.next()).transitional_ignore(); - } - bob->append(StringData{index}, subBob.obj()); - ++index; - break; - } - default: - if (_include) { - bob->appendAs(elt, StringData{index}); - ++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).transitional_ignore(); - } - bob->append(elt.fieldName(), subBob.obj()); - } else { - // Array - BSONObjBuilder matchedBuilder; - if (details && arrayOpType == ARRAY_OP_POSITIONAL) { - // $ positional operator specified - if (!details->hasElemMatchKey()) { - 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 diff --git a/src/mongo/db/exec/projection_exec.h b/src/mongo/db/exec/projection_exec.h deleted file mode 100644 index 784fe2fb209..00000000000 --- a/src/mongo/db/exec/projection_exec.h +++ /dev/null @@ -1,261 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * 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 - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * 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 Server Side 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. - */ - -#pragma once - -#include "boost/optional.hpp" - -#include "mongo/db/exec/document_value/value.h" -#include "mongo/db/exec/working_set.h" -#include "mongo/db/jsobj.h" -#include "mongo/db/matcher/expression.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/util/string_map.h" - -namespace mongo { - -class CollatorInterface; - -/** - * A fully-featured executor for find projection. - */ -class ProjectionExec { -public: - /** - * A .find() projection can have an array operation, either an elemMatch or positional (or - * neither). - */ - enum ArrayOpType { ARRAY_OP_NORMAL = 0, ARRAY_OP_ELEM_MATCH, ARRAY_OP_POSITIONAL }; - - /** - * Projections based on data computed while answering a query, or other metadata about a - * document / query. - */ - enum MetaProjection { - META_GEONEAR_DIST, - META_GEONEAR_POINT, - META_RECORDID, - META_SORT_KEY, - META_TEXT_SCORE, - }; - - /** - * TODO: document why we like StringMap so much here - */ - typedef StringMap<ProjectionExec*> FieldMap; - typedef StringMap<MatchExpression*> Matchers; - typedef StringMap<MetaProjection> MetaMap; - - ProjectionExec(OperationContext* opCtx, - const BSONObj& spec, - const MatchExpression* queryExpression, - const CollatorInterface* collator); - - ~ProjectionExec(); - - /** - * Indicates whether 'sortKey' must be provided for 'computeReturnKeyProjection()' or - * 'project()'. - */ - bool needsSortKey() const { - return _needsSortKey; - } - - /** - * Indicates whether 'geoDistance' must be provided for 'project()'. - */ - bool needsGeoNearDistance() const { - return _needsGeoNearDistance; - } - - /** - * Indicates whether 'geoNearPoint' must be provided for 'project()'. - */ - bool needsGeoNearPoint() const { - return _needsGeoNearPoint; - } - - /** - * Indicates whether 'textScore' is going to be used in 'project()'. - */ - bool needsTextScore() const { - return _needsTextScore; - } - - /** - * Returns false if there are no meta fields to project. - */ - bool hasMetaFields() const { - return !_meta.empty(); - } - - /** - * Performs a projection given a BSONObj source. Meta fields must be provided if necessary. - * Their necessity can be queried via the 'needs*' functions. - */ - StatusWith<BSONObj> project(const BSONObj& in, - const boost::optional<const double> geoDistance = boost::none, - Value geoNearPoint = Value{}, - const BSONObj& sortKey = BSONObj(), - const boost::optional<const double> textScore = boost::none, - const int64_t recordId = 0) const; - - /** - * Performs a projection given index 'KeyData' to directly retrieve results. This function - * handles projections which do not qualify for the ProjectionNodeCovered fast-path but are - * still covered by indices. - */ - StatusWith<BSONObj> projectCovered( - const std::vector<IndexKeyDatum>& keyData, - const boost::optional<const double> geoDistance = boost::none, - Value geoNearPoint = Value{}, - const BSONObj& sortKey = BSONObj(), - const boost::optional<const double> textScore = boost::none, - const int64_t recordId = 0) const; - - /** - * Determines if calls to the project method require that this object was created with the full - * query expression. We may need it for MatchDetails. - */ - bool projectRequiresQueryExpression() const { - return ARRAY_OP_POSITIONAL == _arrayOpType; - } - - -private: - /** - * Adds meta fields to the end of a projection. - */ - BSONObj addMeta(BSONObjBuilder bob, - const boost::optional<const double> geoDistance, - Value geoNearPoint, - const BSONObj& sortKey, - const boost::optional<const double> textScore, - const int64_t recordId) const; - - // - // Initialization - // - - ProjectionExec() = default; - - /** - * Add 'field' as a field name that is included or excluded as part of the projection. - */ - void add(const std::string& field, bool include); - - /** - * Add 'field' as a field name that is sliced as part of the projection. - */ - void add(const std::string& field, int skip, int limit); - - // - // Execution - // - - /** - * Apply the projection that 'this' represents to the object 'in'. 'details' is the result - * of a match evaluation of the full query on the object 'in'. This is only required - * if the projection is positional. - * - * If the projection is successfully computed, returns Status::OK() and stuff the result in - * 'bob'. - * Otherwise, returns error. - */ - Status projectHelper(const BSONObj& in, - BSONObjBuilder* bob, - const MatchDetails* details = nullptr) const; - - /** - * Appends the element 'e' to the builder 'bob', possibly descending into sub-fields of 'e' - * if needed. - */ - Status append(BSONObjBuilder* bob, - const BSONElement& elt, - const MatchDetails* details = nullptr, - const ArrayOpType arrayOpType = ARRAY_OP_NORMAL) const; - - /** - * Like append, but for arrays. - * Deals with slice and calls appendArray to preserve the array-ness. - */ - void appendArray(BSONObjBuilder* bob, const BSONObj& array, bool nested = false) const; - - // True if default at this level is to include. - bool _include = true; - - // True if this level can't be skipped or included without recursing. - bool _special = false; - - // We must group projections with common prefixes together. - // TODO: benchmark std::vector<pair> vs map - // - // Projection is a rooted tree. If we have {a.b: 1, a.c: 1} we don't want to - // double-traverse the document when we're projecting it. Instead, we have an entry in - // _fields for 'a' with two sub projections: b:1 and c:1. - FieldMap _fields; - - // The raw projection spec. that is passed into init(...) - BSONObj _source; - - // Should we include the _id field? - bool _includeID = true; - - // Arguments from the $slice operator. - int _skip = 0; - int _limit = -1; - - // Used for $elemMatch and positional operator ($) - Matchers _matchers; - - // The matchers above point into BSONObjs and this is where those objs live. - std::vector<BSONObj> _elemMatchObjs; - - ArrayOpType _arrayOpType = ARRAY_OP_NORMAL; - - // The full query expression. Used when we need MatchDetails. - const MatchExpression* _queryExpression = nullptr; - - // Projections that aren't sourced from the document or index keys. - MetaMap _meta; - - // After parsing in the constructor, these fields will indicate the neccesity of metadata - // for $meta projection. - bool _needsSortKey = false; - bool _needsGeoNearDistance = false; - bool _needsGeoNearPoint = false; - bool _needsTextScore = false; - - // The collator this projection should use to compare strings. Needed for projection operators - // that perform matching (e.g. elemMatch projection). If null, the collation is a simple binary - // compare. - const CollatorInterface* _collator = nullptr; -}; - -} // namespace mongo diff --git a/src/mongo/db/exec/projection_exec_test.cpp b/src/mongo/db/exec/projection_exec_test.cpp deleted file mode 100644 index f15094fd3aa..00000000000 --- a/src/mongo/db/exec/projection_exec_test.cpp +++ /dev/null @@ -1,310 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * 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 - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * 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 Server Side 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. - */ - -/** - * This file contains tests for mongo/db/exec/projection_exec.cpp - */ - -#include "boost/optional.hpp" -#include "boost/optional/optional_io.hpp" -#include <memory> - -#include "mongo/db/exec/projection_exec.h" - -#include "mongo/db/json.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/db/query/collation/collator_interface_mock.h" -#include "mongo/stdx/variant.h" -#include "mongo/unittest/unittest.h" - -using namespace mongo; -using namespace std::string_literals; - -namespace { - -/** - * Utility function to create a MatchExpression. - */ -std::unique_ptr<MatchExpression> parseMatchExpression(const BSONObj& obj) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - StatusWithMatchExpression status = MatchExpressionParser::parse(obj, std::move(expCtx)); - ASSERT_TRUE(status.isOK()); - return std::move(status.getValue()); -} - -/** - * Test encapsulation for single call to ProjectionExec::project() or - * ProjectionExec::projectCovered(). - */ -boost::optional<std::string> project( - const char* specStr, - const char* queryStr, - const stdx::variant<const char*, const IndexKeyDatum> objStrOrDatum, - const boost::optional<const CollatorInterface&> collator = boost::none, - const BSONObj& sortKey = BSONObj(), - const double textScore = 0.0) { - // Create projection exec object. - BSONObj spec = fromjson(specStr); - BSONObj query = fromjson(queryStr); - std::unique_ptr<MatchExpression> queryExpression = parseMatchExpression(query); - QueryTestServiceContext serviceCtx; - auto opCtx = serviceCtx.makeOperationContext(); - ProjectionExec exec(opCtx.get(), spec, queryExpression.get(), collator.get_ptr()); - - auto objStr = stdx::get_if<const char*>(&objStrOrDatum); - auto projected = objStr - ? exec.project(fromjson(*objStr), boost::none, Value{}, sortKey, textScore) - : exec.projectCovered({stdx::get<const IndexKeyDatum>(objStrOrDatum)}, - boost::none, - Value{}, - sortKey, - textScore); - - if (!projected.isOK()) - return boost::none; - else - return boost::make_optional(projected.getValue().toString()); -} - -// -// position $ -// - -TEST(ProjectionExecTest, TransformPositionalDollar) { - // Valid position $ projections. - ASSERT_EQ(boost::make_optional("{ a: [ 10 ] }"s), - project("{'a.$': 1}", "{a: 10}", "{a: [10, 20, 30]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 20 ] }"s), - project("{'a.$': 1}", "{a: 20}", "{a: [10, 20, 30]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 5 ] }"s), - project("{'a.$': 1}", "{a: {$gt: 4}}", "{a: [5]}")); - - // Invalid position $ projections. - ASSERT_EQ(boost::none, project("{'a.$': 1}", "{a: {$size: 1}}", "{a: [5]}")); -} - -// -// $elemMatch -// - -TEST(ProjectionExecTest, TransformElemMatch) { - const char* s = "{a: [{x: 1, y: 10}, {x: 1, y: 20}, {x: 2, y: 10}]}"; - - // Valid $elemMatch projections. - ASSERT_EQ(boost::make_optional("{ a: [ { x: 1, y: 10 } ] }"s), - project("{a: {$elemMatch: {x: 1}}}", "{}", s)); - ASSERT_EQ(boost::make_optional("{ a: [ { x: 1, y: 20 } ] }"s), - project("{a: {$elemMatch: {x: 1, y: 20}}}", "{}", s)); - ASSERT_EQ(boost::make_optional("{ a: [ { x: 2, y: 10 } ] }"s), - project("{a: {$elemMatch: {x: 2}}}", "{}", s)); - ASSERT_EQ(boost::make_optional("{}"s), project("{a: {$elemMatch: {x: 3}}}", "{}", s)); - - // $elemMatch on unknown field z - ASSERT_EQ(boost::make_optional("{}"s), project("{a: {$elemMatch: {z: 1}}}", "{}", s)); -} - -TEST(ProjectionExecTest, ElemMatchProjectionRespectsCollator) { - CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); - ASSERT_EQ(boost::make_optional("{ a: [ \"zdd\" ] }"s), - project("{a: {$elemMatch: {$gte: 'abc'}}}", - "{}", - "{a: ['zaa', 'zbb', 'zdd', 'zee']}", - collator)); -} - -// -// $slice -// - -TEST(ProjectionExecTest, TransformSliceCount) { - // Valid $slice projections using format {$slice: count}. - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6, 8 ] }"s), - project("{a: {$slice: -10}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6, 8 ] }"s), - project("{a: {$slice: -3}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 8 ] }"s), - project("{a: {$slice: -1}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [] }"s), - project("{a: {$slice: 0}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4 ] }"s), - project("{a: {$slice: 1}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6, 8 ] }"s), - project("{a: {$slice: 3}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6, 8 ] }"s), - project("{a: {$slice: 10}}", "{}", "{a: [4, 6, 8]}")); -} - -TEST(ProjectionExecTest, TransformSliceSkipLimit) { - // Valid $slice projections using format {$slice: [skip, limit]}. - // Non-positive limits are rejected at the query parser and therefore not handled by - // the projection execution stage. In fact, it will abort on an invalid limit. - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6, 8 ] }"s), - project("{a: {$slice: [-10, 10]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6, 8 ] }"s), - project("{a: {$slice: [-3, 5]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 8 ] }"s), - project("{a: {$slice: [-1, 1]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4, 6 ] }"s), - project("{a: {$slice: [0, 2]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 4 ] }"s), - project("{a: {$slice: [0, 1]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [ 6 ] }"s), - project("{a: {$slice: [1, 1]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [] }"s), - project("{a: {$slice: [3, 5]}}", "{}", "{a: [4, 6, 8]}")); - ASSERT_EQ(boost::make_optional("{ a: [] }"s), - project("{a: {$slice: [10, 10]}}", "{}", "{a: [4, 6, 8]}")); -} - -// -// Dotted projections. -// - -TEST(ProjectionExecTest, TransformCoveredDottedProjection) { - BSONObj keyPattern = fromjson("{a: 1, 'b.c': 1, 'b.d': 1, 'b.f.g': 1, 'b.f.h': 1}"); - BSONObj keyData = fromjson("{'': 1, '': 2, '': 3, '': 4, '': 5}"); - ASSERT_EQ(boost::make_optional("{ b: { c: 2, d: 3, f: { g: 4, h: 5 } } }"s), - project("{'b.c': 1, 'b.d': 1, 'b.f.g': 1, 'b.f.h': 1}", - "{}", - IndexKeyDatum(keyPattern, keyData, 0, SnapshotId{}))); -} - -TEST(ProjectionExecTest, TransformNonCoveredDottedProjection) { - ASSERT_EQ(boost::make_optional("{ b: { c: 2, d: 3, f: { g: 4, h: 5 } } }"s), - project("{'b.c': 1, 'b.d': 1, 'b.f.g': 1, 'b.f.h': 1}", - "{}", - "{a: 1, b: {c: 2, d: 3, f: {g: 4, h: 5}}}")); -} - -// -// $meta -// $meta projections add computed values to the projected object. -// - -TEST(ProjectionExecTest, TransformMetaTextScore) { - // Query {} is ignored. - ASSERT_EQ(boost::make_optional("{ a: \"hello\", b: 100.0 }"s), - project("{b: {$meta: 'textScore'}}", - "{}", - "{a: 'hello'}", - boost::none, // collator - BSONObj(), // sortKey - 100.0)); // textScore - // Projected meta field should overwrite existing field. - ASSERT_EQ(boost::make_optional("{ a: \"hello\", b: 100.0 }"s), - project("{b: {$meta: 'textScore'}}", - "{}", - "{a: 'hello', b: -1}", - boost::none, // collator - BSONObj(), // sortKey - 100.0)); // textScore -} - -TEST(ProjectionExecTest, TransformMetaSortKey) { - // Query {} is ignored. - ASSERT_EQ(boost::make_optional("{ a: \"hello\", b: { : 99 } }"s), - project("{b: {$meta: 'sortKey'}}", - "{}", - "{a: 'hello'}", - boost::none, // collator - BSON("" << 99))); // sortKey - - // Projected meta field should overwrite existing field. - ASSERT_EQ(boost::make_optional("{ a: { : 99 } }"s), - project("{a: {$meta: 'sortKey'}}", - "{}", - "{a: 'hello'}", - boost::none, // collator - BSON("" << 99))); // sortKey -} - -TEST(ProjectionExecTest, TransformMetaSortKeyCoveredNormal) { - ASSERT_EQ(boost::make_optional("{ a: 5, b: { : 5 } }"s), - project("{_id: 0, a: 1, b: {$meta: 'sortKey'}}", - "{}", - IndexKeyDatum(BSON("a" << 1), BSON("" << 5), 0, SnapshotId{}), - boost::none, // collator - BSON("" << 5))); // sortKey -} - -TEST(ProjectionExecTest, TransformMetaSortKeyCoveredOverwrite) { - ASSERT_EQ(boost::make_optional("{ a: { : 5 } }"s), - project("{_id: 0, a: 1, a: {$meta: 'sortKey'}}", - "{}", - IndexKeyDatum(BSON("a" << 1), BSON("" << 5), 0, SnapshotId{}), - boost::none, // collator - BSON("" << 5))); // sortKey -} - -TEST(ProjectionExecTest, TransformMetaSortKeyCoveredAdditionalData) { - ASSERT_EQ(boost::make_optional("{ a: 5, c: 6, b: { : 5 } }"s), - project("{_id: 0, a: 1, b: {$meta: 'sortKey'}, c: 1}", - "{}", - IndexKeyDatum( - BSON("a" << 1 << "c" << 1), BSON("" << 5 << "" << 6), 0, SnapshotId{}), - boost::none, // collator - BSON("" << 5))); // sortKey -} - -TEST(ProjectionExecTest, TransformMetaSortKeyCoveredCompound) { - ASSERT_EQ(boost::make_optional("{ a: 5, b: { : 5, : 6 } }"s), - project("{_id: 0, a: 1, b: {$meta: 'sortKey'}}", - "{}", - IndexKeyDatum( - BSON("a" << 1 << "c" << 1), BSON("" << 5 << "" << 6), 0, SnapshotId{}), - boost::none, // collator - BSON("" << 5 << "" << 6))); // sortKey -} - -TEST(ProjectionExecTest, TransformMetaSortKeyCoveredCompound2) { - ASSERT_EQ(boost::make_optional("{ a: 5, c: 4, b: { : 5, : 6 } }"s), - project("{_id: 0, a: 1, c: 1, b: {$meta: 'sortKey'}}", - "{}", - IndexKeyDatum(BSON("a" << 1 << "b" << 1 << "c" << 1), - BSON("" << 5 << "" << 6 << "" << 4), - 0, - SnapshotId{}), - boost::none, // collator - BSON("" << 5 << "" << 6))); // sortKey -} - -TEST(ProjectionExecTest, TransformMetaSortKeyCoveredCompound3) { - ASSERT_EQ(boost::make_optional("{ c: 4, d: 9000, b: { : 6, : 4 } }"s), - project("{_id: 0, c: 1, d: 1, b: {$meta: 'sortKey'}}", - "{}", - IndexKeyDatum(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1), - BSON("" << 5 << "" << 6 << "" << 4 << "" << 9000), - 0, - SnapshotId{}), - boost::none, // collator - BSON("" << 6 << "" << 4))); // sortKey -} - -} // namespace diff --git a/src/mongo/db/exec/projection_executor_test.cpp b/src/mongo/db/exec/projection_executor_test.cpp index a1ac7467510..0caa4f3d007 100644 --- a/src/mongo/db/exec/projection_executor_test.cpp +++ b/src/mongo/db/exec/projection_executor_test.cpp @@ -32,6 +32,7 @@ #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/projection_executor.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" +#include "mongo/db/query/collation/collator_interface_mock.h" #include "mongo/db/query/projection_ast_util.h" #include "mongo/db/query/projection_parser.h" #include "mongo/unittest/unittest.h" @@ -134,6 +135,9 @@ TEST_F(ProjectionExecutorTest, CanProjectFindPositional) { auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [3]}}")}, executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}}")})); + + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [4]}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [4, 3, 2]}}")})); } TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithInclusion) { @@ -144,6 +148,37 @@ TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithInclusion) { executor->applyTransformation(Document{fromjson("{a: [{b: 1}, {b: 2}, {b: 3}]}")})); } +TEST_F(ProjectionExecutorTest, CanProjectFindElemMatch) { + + const BSONObj obj = fromjson("{a: [{b: 3, c: 1}, {b: 1, c: 2}, {b: 1, c: 3}]}"); + { + auto proj = parseWithDefaultPolicies(fromjson("{a: {$elemMatch: {b: 1}}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 2}]}")}, + executor->applyTransformation(Document{obj})); + } + + { + auto proj = parseWithDefaultPolicies(fromjson("{a: {$elemMatch: {b: 1, c: 3}}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 3}]}")}, + executor->applyTransformation(Document{obj})); + } +} + +TEST_F(ProjectionExecutorTest, ElemMatchRespectsCollator) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); + getExpCtx()->setCollator(&collator); + + auto proj = parseWithDefaultPolicies(fromjson("{a: {$elemMatch: {$gte: 'abc'}}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + + + ASSERT_DOCUMENT_EQ( + Document{fromjson("{ a: [ \"zdd\" ] }")}, + executor->applyTransformation(Document{fromjson("{a: ['zaa', 'zbb', 'zdd', 'zee']}")})); +} + TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithExclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 0}")); auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); @@ -156,11 +191,27 @@ TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithInclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [1,2,3]}, c: 'abc'}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3]}, c: 'abc'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithInclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithExclusion) { +TEST_F(ProjectionExecutorTest, CanProjectFindSliceBasicWithExclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: 3}, c: 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [1,2,3]}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithExclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 0}")); auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); ASSERT_DOCUMENT_EQ( |