summaryrefslogtreecommitdiff
path: root/src/mongo/db/exec
diff options
context:
space:
mode:
authorIan Boros <ian.boros@mongodb.com>2019-10-23 22:04:36 +0000
committerevergreen <evergreen@mongodb.com>2019-10-23 22:04:36 +0000
commitfcff61405cffec61340caf4090615317b5ac5991 (patch)
tree4f98476c83b72b8ac4273bd2a03bd747bd0e7881 /src/mongo/db/exec
parentec1cad447dffe1ffcce14ab03e268222dfd32d01 (diff)
downloadmongo-fcff61405cffec61340caf4090615317b5ac5991.tar.gz
SERVER-42435 Remove ParsedProjection and ProjectionExec
Diffstat (limited to 'src/mongo/db/exec')
-rw-r--r--src/mongo/db/exec/SConscript1
-rw-r--r--src/mongo/db/exec/projection.cpp4
-rw-r--r--src/mongo/db/exec/projection_exec.cpp489
-rw-r--r--src/mongo/db/exec/projection_exec.h261
-rw-r--r--src/mongo/db/exec/projection_exec_test.cpp310
-rw-r--r--src/mongo/db/exec/projection_executor_test.cpp53
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(