/**
* Copyright (C) 2013 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include "mongo/db/exec/projection_exec.h"
#include "mongo/db/exec/working_set_computed_data.h"
#include "mongo/db/matcher/expression_parser.h"
#include "mongo/db/matcher/expression.h"
#include "mongo/db/query/lite_parsed_query.h"
#include "mongo/util/mongoutils/str.h"
namespace mongo {
using std::max;
using std::string;
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,
const MatchExpressionParser::WhereCallback& whereCallback)
: _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,
whereCallback);
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(member->getComputed(WSM_INDEX_KEY));
keyObj = key->getKey();
}
member->state = WorkingSetMember::OWNED_OBJ;
member->obj = keyObj;
member->keyData.clear();
member->loc = RecordId();
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(
member->getComputed(WSM_COMPUTED_GEO_DISTANCE));
bob.append(it->first, dist->getDist());
}
else {
return Status(ErrorCodes::InternalError,
"near loc dist requested but no data available");
}
}
else if (META_GEONEAR_POINT == it->second) {
if (member->hasComputed(WSM_GEO_NEAR_POINT)) {
const GeoNearPointComputedData* point
= static_cast(
member->getComputed(WSM_GEO_NEAR_POINT));
BSONObj ptObj = point->getPoint();
if (ptObj.couldBeArray()) {
bob.appendArray(it->first, ptObj);
}
else {
bob.append(it->first, ptObj);
}
}
else {
return Status(ErrorCodes::InternalError,
"near loc proj requested but no data available");
}
}
else if (META_TEXT_SCORE == it->second) {
if (member->hasComputed(WSM_COMPUTED_TEXT_SCORE)) {
const TextScoreComputedData* score
= static_cast(
member->getComputed(WSM_COMPUTED_TEXT_SCORE));
bob.append(it->first, score->getScore());
}
else {
bob.append(it->first, 0.0);
}
}
else if (META_DISKLOC == it->second) {
// For compatibility with old versions, we output as a split DiskLoc.
const int64_t repr = member->loc.repr();
BSONObjBuilder sub(bob.subobjStart(it->first));
sub.append("file", int(repr >> 32));
sub.append("offset", int(uint32_t(repr)));
}
}
BSONObj newObj = bob.obj();
member->state = WorkingSetMember::OWNED_OBJ;
member->obj = newObj;
member->keyData.clear();
member->loc = RecordId();
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