diff options
author | Ted Tuckman <ted.tuckman@mongodb.com> | 2021-01-21 14:44:35 -0500 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-01-25 15:57:46 +0000 |
commit | 2497bf18a4f2959501582f4084ad27212698b574 (patch) | |
tree | aaf8c1f5dc5e0ae6fb0eeaa2796ee1bd468a65a8 | |
parent | dc70bfe933ba9f4b01a028eab72e44e2681fa09a (diff) | |
download | mongo-2497bf18a4f2959501582f4084ad27212698b574.tar.gz |
SERVER-52814 Serialize projection _id field to always equivalent projection spec
9 files changed, 197 insertions, 38 deletions
diff --git a/buildscripts/resmokeconfig/suites/cst_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/cst_jscore_passthrough.yml index b53de16836d..8dd8eab5389 100755 --- a/buildscripts/resmokeconfig/suites/cst_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/cst_jscore_passthrough.yml @@ -26,6 +26,7 @@ selector: - jstests/core/wildcard_index_collation.js - jstests/core/wildcard_index_hint.js - jstests/core/wildcard_index_return_key.js + - jstests/core/wildcard_index_projection.js - jstests/core/update_array_offset_positional.js - jstests/core/update_arraymatch6.js - jstests/core/update_arraymatch8.js diff --git a/jstests/core/wildcard_index_projection.js b/jstests/core/wildcard_index_projection.js new file mode 100644 index 00000000000..f460852165d --- /dev/null +++ b/jstests/core/wildcard_index_projection.js @@ -0,0 +1,68 @@ +/** + * Tests that a wildcard index with an exclusion projection but including _id field gets saved + * properly. Exercises the fix for SERVER-52814. + * @tags: [ + * sbe_incompatible, + * requires_fcv_49, + * ] + */ + +(function() { +"use strict"; + +load("jstests/libs/fixture_helpers.js"); // For isMongos. + +const collName = jsTestName(); +const coll = db[collName]; +coll.drop(); +coll.createIndex({"$**": 1}, {wildcardProjection: {name: 0, type: 0, _id: 1}}); + +const sharded = FixtureHelpers.isMongos(db); + +// Return the explain object containing the winning plan and rejected plans. +function getExplainObj(explainRes) { + if (sharded) { + return explainRes.queryPlanner.winningPlan.shards[0]; + } + + return explainRes.queryPlanner; +} + +const indexes = coll.getIndexes().filter(idx => idx.name === "$**_1"); +assert.eq(1, indexes.length); +const indexSpec = indexes[0]; +assert.eq(false, indexSpec.wildcardProjection.name, indexes); +assert.eq(false, indexSpec.wildcardProjection.type, indexes); +assert.eq(true, indexSpec.wildcardProjection._id, indexes); + +coll.insert({name: "Ted", type: "Person", _id: 1}); +coll.insert({name: "Bernard", type: "Person", _id: 2}); +const explainResFull = coll.find({_id: {$eq: 1}}).explain(); +const plannerRes = getExplainObj(explainResFull); +// For a query on _id we expect that the IDHACK plan will be selected. However, we should also +// observe a rejected plan which uses the wildcard index to resolve _id. In a sharded cluster we +// may also need to skip the _id: hashed index. +let indexStage = plannerRes.rejectedPlans[0].inputStage; +if (sharded) { + if (indexStage.keyPattern._id === "hashed") { + assert.eq(plannerRes.rejectedPlans.length, 2, plannerRes.rejectedPlans); + indexStage = plannerRes.rejectedPlans[1].inputStage; + } +} else { + assert.eq(plannerRes.rejectedPlans.length, 1, plannerRes.rejectedPlans); +} + +assert.eq(indexStage.stage, "IXSCAN", indexStage); +assert.eq(indexStage.keyPattern, {"$_path": 1, "_id": 1}, indexStage); + +// Ensure we use the index for _id if we supply a hint. +const hintExplainRes = coll.find({_id: {$eq: 1}}).hint("$**_1").explain(); +const winningPlan = getExplainObj(hintExplainRes).winningPlan; +assert.eq(winningPlan.inputStage.stage, "IXSCAN", winningPlan.inputStage); +assert.eq(winningPlan.inputStage.keyPattern, {$_path: 1, _id: 1}, winningPlan.inputStage); + +// Test that the results are correct. +const hintedResults = coll.find({_id: {$eq: 1}}).hint("$**_1").toArray(); +assert.eq(hintedResults.length, 1, hintedResults); +assert.eq(hintedResults[0]._id, 1, hintedResults); +})(); diff --git a/src/mongo/db/cst/cst_pipeline_translation_test.cpp b/src/mongo/db/cst/cst_pipeline_translation_test.cpp index 0bd77dd3846..3d247af1e57 100644 --- a/src/mongo/db/cst/cst_pipeline_translation_test.cpp +++ b/src/mongo/db/cst/cst_pipeline_translation_test.cpp @@ -193,7 +193,7 @@ TEST(CstPipelineTranslationTest, TranslatesOneFieldExclusionProjectionStage) { auto& singleDoc = dynamic_cast<DocumentSourceSingleDocumentTransformation&>(**iter); // DocumenSourceSingleDocumentTransformation reorders fields so we need to be insensitive. ASSERT(UnorderedFieldsBSONObjComparator{}.evaluate( - BSON("a" << false) == + BSON("a" << false << "_id" << true) == singleDoc.getTransformer().serializeTransformation(boost::none).toBson())); } @@ -244,7 +244,8 @@ TEST(CstPipelineTranslationTest, TranslatesCompoundObjectExclusionProjection) { auto& singleDoc = dynamic_cast<DocumentSourceSingleDocumentTransformation&>(**iter); // DocumenSourceSingleDocumentTransformation reorders fields so we need to be insensitive. ASSERT(UnorderedFieldsBSONObjComparator{}.evaluate( - BSON("a" << BSON("b" << BSON("c" << false << "d" << false << "e" << BSON("f" << false)))) == + BSON("a" << BSON("b" << BSON("c" << false << "d" << false << "e" << BSON("f" << false))) + << "_id" << true) == singleDoc.getTransformer().serializeTransformation(boost::none).toBson())); } @@ -263,7 +264,7 @@ TEST(CstPipelineTranslationTest, TranslatesMultiComponentPathExclusionProjection auto& singleDoc = dynamic_cast<DocumentSourceSingleDocumentTransformation&>(**iter); // DocumenSourceSingleDocumentTransformation reorders fields so we need to be insensitive. ASSERT(UnorderedFieldsBSONObjComparator{}.evaluate( - BSON("a" << BSON("b" << BSON("c" << BSON("d" << false)))) == + BSON("a" << BSON("b" << BSON("c" << BSON("d" << false))) << "_id" << true) == singleDoc.getTransformer().serializeTransformation(boost::none).toBson())); } @@ -399,7 +400,7 @@ TEST(CstPipelineTranslationTest, TranslatesMultipleProjectionStages) { auto& singleDoc = dynamic_cast<DocumentSourceSingleDocumentTransformation&>(**iter++); // DocumenSourceSingleDocumentTransformation reorders fields so we need to be insensitive. ASSERT(UnorderedFieldsBSONObjComparator{}.evaluate( - BSON("b" << false) == + BSON("b" << false << "_id" << true) == singleDoc.getTransformer().serializeTransformation(boost::none).toBson())); } { diff --git a/src/mongo/db/exec/exclusion_projection_executor.h b/src/mongo/db/exec/exclusion_projection_executor.h index cf21bf57126..dd4854e39ea 100644 --- a/src/mongo/db/exec/exclusion_projection_executor.h +++ b/src/mongo/db/exec/exclusion_projection_executor.h @@ -106,7 +106,16 @@ public: Document serializeTransformation( boost::optional<ExplainOptions::Verbosity> explain) const final { - return _root->serialize(explain); + MutableDocument output; + + // The ExclusionNode tree in '_root' will always have a top-level _id node if _id is to be + // excluded. If the _id node is not present, then explicitly set {_id: true} to avoid + // ambiguity in the expected behavior of the serialized projection. + _root->serialize(explain, &output); + if (output.peek()["_id"].missing()) { + output.addField("_id", Value{true}); + } + return output.freeze(); } /** diff --git a/src/mongo/db/exec/exclusion_projection_executor_test.cpp b/src/mongo/db/exec/exclusion_projection_executor_test.cpp index 51893b71838..9ddd696e861 100644 --- a/src/mongo/db/exec/exclusion_projection_executor_test.cpp +++ b/src/mongo/db/exec/exclusion_projection_executor_test.cpp @@ -106,6 +106,43 @@ TEST(ExclusionProjectionExecutionTest, ShouldSerializeToEquivalentProjection) { ASSERT_VALUE_EQ(serialization["x"].getDocument()["y"], Value(false)); } +TEST(ExclusionProjectionExecutionTest, ShouldSerializeWithTopLevelID) { + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a" << 0 << "b" << 0)); + auto serialization = exclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(false)); + ASSERT_VALUE_EQ(serialization["b"], Value(false)); + ASSERT_VALUE_EQ(serialization["_id"], Value(true)); + + exclusion = makeExclusionProjectionWithDefaultPolicies( + BSON("a" << 0 << "b" << BSON("c" << 0 << "d" << 0))); + serialization = exclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(false)); + ASSERT_VALUE_EQ(serialization["b"]["c"], Value(false)); + ASSERT_VALUE_EQ(serialization["b"]["d"], Value(false)); + ASSERT_VALUE_EQ(serialization["_id"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"]["_id"], Value()); + + exclusion = makeExclusionProjectionWithDefaultIdExclusion(BSON("a" << false << "b" << false)); + serialization = exclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(false)); + ASSERT_VALUE_EQ(serialization["b"], Value(false)); + ASSERT_VALUE_EQ(serialization["_id"], Value(false)); + + exclusion = makeExclusionProjectionWithDefaultIdExclusion( + BSON("a" << false << "b" << false << "_id" << false)); + serialization = exclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(false)); + ASSERT_VALUE_EQ(serialization["b"], Value(false)); + ASSERT_VALUE_EQ(serialization["_id"], Value(false)); + + exclusion = makeExclusionProjectionWithDefaultIdExclusion( + BSON("a" << false << "b" << false << "_id" << true)); + serialization = exclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(false)); + ASSERT_VALUE_EQ(serialization["b"], Value(false)); + ASSERT_VALUE_EQ(serialization["_id"], Value(true)); +} + TEST(ExclusionProjectionExecutionTest, ShouldNotAddAnyDependencies) { // An exclusion projection will cause the stage to return DepsTracker::State::SEE_NEXT, meaning // it doesn't strictly require any fields. diff --git a/src/mongo/db/exec/inclusion_projection_executor.h b/src/mongo/db/exec/inclusion_projection_executor.h index b095a423652..c21196d11e5 100644 --- a/src/mongo/db/exec/inclusion_projection_executor.h +++ b/src/mongo/db/exec/inclusion_projection_executor.h @@ -169,6 +169,9 @@ public: boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument output; + // The InclusionNode tree in '_root' will always have a top-level _id node if _id is to be + // included. If the _id node is not present, then explicitly set {_id: false} to avoid + // ambiguity in the expected behavior of the serialized projection. _root->serialize(explain, &output); if (output.peek()["_id"].missing()) { output.addField("_id", Value{false}); diff --git a/src/mongo/db/exec/inclusion_projection_executor_test.cpp b/src/mongo/db/exec/inclusion_projection_executor_test.cpp index 1b61ab11f9b..d80c741e0c0 100644 --- a/src/mongo/db/exec/inclusion_projection_executor_test.cpp +++ b/src/mongo/db/exec/inclusion_projection_executor_test.cpp @@ -305,6 +305,43 @@ TEST_F(InclusionProjectionExecutionTestWithoutFallBackToDefault, inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } +TEST_F(InclusionProjectionExecutionTestWithoutFallBackToDefault, ShouldSerializeWithTopLevelID) { + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << 1 << "b" << 1)); + auto serialization = inclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"], Value(true)); + ASSERT_VALUE_EQ(serialization["_id"], Value(true)); + + inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a" << 1 << "b" << BSON("c" << 1 << "d" << 1))); + serialization = inclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"]["c"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"]["d"], Value(true)); + ASSERT_VALUE_EQ(serialization["_id"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"]["_id"], Value()); + + inclusion = makeInclusionProjectionWithDefaultIdExclusion(BSON("a" << true << "b" << true)); + serialization = inclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"], Value(true)); + ASSERT_VALUE_EQ(serialization["_id"], Value(false)); + + inclusion = makeInclusionProjectionWithDefaultIdExclusion( + BSON("a" << true << "b" << true << "_id" << false)); + serialization = inclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"], Value(true)); + ASSERT_VALUE_EQ(serialization["_id"], Value(false)); + + inclusion = makeInclusionProjectionWithDefaultIdExclusion( + BSON("a" << true << "b" << true << "_id" << true)); + serialization = inclusion->serializeTransformation(boost::none); + ASSERT_VALUE_EQ(serialization["a"], Value(true)); + ASSERT_VALUE_EQ(serialization["b"], Value(true)); + ASSERT_VALUE_EQ(serialization["_id"], Value(true)); +} + TEST_F(InclusionProjectionExecutionTestWithFallBackToDefault, ShouldOptimizeTopLevelExpressions) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2)))); diff --git a/src/mongo/db/pipeline/document_source_project_test.cpp b/src/mongo/db/pipeline/document_source_project_test.cpp index 685de1e3603..ced99ce0200 100644 --- a/src/mongo/db/pipeline/document_source_project_test.cpp +++ b/src/mongo/db/pipeline/document_source_project_test.cpp @@ -430,7 +430,7 @@ TEST_F(UnsetTest, UnsetSerializesToProject) { vector<Value> serializedArray; unsetStage->serializeToArray(serializedArray); auto serializedUnsetStage = serializedArray[0].getDocument().toBson(); - ASSERT_BSONOBJ_EQ(serializedUnsetStage, fromjson("{$project: {b: {c: false}}}")); + ASSERT_BSONOBJ_EQ(serializedUnsetStage, fromjson("{$project: {b: {c: false}, _id: true}}")); auto projectStage = DocumentSourceProject::createFromBson(serializedUnsetStage.firstElement(), getExpCtx()); projectStage->serializeToArray(serializedArray); diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp index 93813da0e66..62ec234c501 100644 --- a/src/mongo/db/pipeline/pipeline_test.cpp +++ b/src/mongo/db/pipeline/pipeline_test.cpp @@ -1225,14 +1225,17 @@ TEST(PipelineOptimizationTest, GraphLookupShouldSwapWithMatch) { TEST(PipelineOptimizationTest, ExclusionProjectShouldSwapWithIndependentMatch) { string inputPipe = "[{$project: {redacted: 0}}, {$match: {unrelated: 4}}]"; - string outputPipe = "[{$match: {unrelated: {$eq : 4}}}, {$project: {redacted: false}}]"; - string serializedPipe = "[{$match : {unrelated : 4}}, {$project : {redacted : false}}]"; + string outputPipe = + "[{$match: {unrelated: {$eq : 4}}}, {$project: {redacted: false, _id: true}}]"; + string serializedPipe = + "[{$match : {unrelated : 4}}, {$project : {redacted : false, _id: true}}]"; assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); } TEST(PipelineOptimizationTest, ExclusionProjectShouldNotSwapWithMatchOnExcludedFields) { std::string pipeline = - "[{$project: {subdoc: {redacted: false}}}, {$match: {'subdoc.redacted': {$eq : 4}}}]"; + "[{$project: {subdoc: {redacted: false}, _id: true}}, {$match: {'subdoc.redacted': {$eq : " + "4}}}]"; assertPipelineOptimizesTo(pipeline, pipeline); } @@ -1242,7 +1245,7 @@ TEST(PipelineOptimizationTest, MatchShouldSplitIfPartIsIndependentOfExclusionPro " {$match: {redacted: 'x', unrelated: 4}}]"; string outputPipe = "[{$match: {unrelated: {$eq: 4}}}," - " {$project: {redacted: false}}," + " {$project: {redacted: false, _id: true}}," " {$match: {redacted: {$eq: 'x'}}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); } @@ -1452,7 +1455,7 @@ TEST(PipelineOptimizationTest, MatchOnMinItemsShouldNotSwapSinceCategoryIsArrayM assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaMinItems: 1}}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1471,7 +1474,7 @@ TEST(PipelineOptimizationTest, MatchOnMaxItemsShouldNotSwapSinceCategoryIsArrayM assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaMaxItems: 1}}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1491,7 +1494,7 @@ TEST(PipelineOptimizationTest, assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaAllElemMatchFromIndex: [1, {b: {$gt: 0}}]}}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1511,7 +1514,7 @@ TEST(PipelineOptimizationTest, MatchOnArrayIndexShouldNotSwapSinceCategoryIsArra assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = R"( - [{$project: {redacted: false}}, + [{$project: {redacted: false, _id: true}}, {$match: {a: {$_internalSchemaMatchArrayIndex: {index: 0, namePlaceholder: 'i', expression: {i: {$lt: 0}}}}}}])"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1532,7 +1535,7 @@ TEST(PipelineOptimizationTest, MatchOnUniqueItemsShouldNotSwapSinceCategoryIsArr assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaUniqueItems: true}}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1561,10 +1564,10 @@ TEST(PipelineOptimizationTest, MatchOnObjectMatchShouldNotSwapSinceCategoryIsOth "[{$project: {redacted: false}}, " "{$match: {a: {$_internalSchemaObjectMatch: {b: 1}}}}]"; outputPipe = - "[{$project: {redacted: false}}," + "[{$project: {redacted: false, _id: true}}," "{$match: {a: {$_internalSchemaObjectMatch: {b: {$eq: 1}}}}}]"; serializedPipe = - "[{$project: {redacted: false}}," + "[{$project: {redacted: false, _id: true}}," "{$match: {a: {$_internalSchemaObjectMatch: {b: 1}}}}]"; assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); @@ -1591,7 +1594,7 @@ TEST(PipelineOptimizationTest, MatchOnMinPropertiesShouldNotSwapSinceCategoryIsO assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {$_internalSchemaMinProperties: 2}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1610,7 +1613,7 @@ TEST(PipelineOptimizationTest, MatchOnMaxPropertiesShouldNotSwapSinceCategoryIsO assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {$_internalSchemaMaxProperties: 2}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1658,7 +1661,7 @@ TEST(PipelineOptimizationTest, MatchOnAllowedPropertiesShouldNotSwapSinceCategor otherwise: {i: 1} }}}])"; outputPipe = R"( - [{$project: {redacted: false}}, + [{$project: {redacted: false, _id: true}}, {$match: {$_internalSchemaAllowedProperties: { properties: ['b'], namePlaceholder: 'i', @@ -1666,7 +1669,7 @@ TEST(PipelineOptimizationTest, MatchOnAllowedPropertiesShouldNotSwapSinceCategor otherwise: {i: {$eq: 1} }}}}])"; serializedPipe = R"( - [{$project: {redacted: false}}, + [{$project: {redacted: false, _id: true}}, {$match: {$_internalSchemaAllowedProperties: { properties: ['b'], namePlaceholder: 'i', @@ -1720,10 +1723,10 @@ TEST(PipelineOptimizationTest, MatchOnCondShouldNotSwapSinceCategoryIsOther) { "[{$project: {redacted: false}}, " "{$match: {$_internalSchemaCond: [{a: 1}, {b: 1}, {c: 1}]}}]"; outputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {$_internalSchemaCond: [{a: {$eq : 1}}, {b: {$eq: 1}}, {c: {$eq: 1}}]}}]"; serializedPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {$_internalSchemaCond: [{a: 1}, {b: 1}, {c: 1}]}}]"; assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); @@ -1748,7 +1751,7 @@ TEST(PipelineOptimizationTest, MatchOnRootDocEqShouldNotSwapSinceCategoryIsOther assertPipelineOptimizesTo(inputPipe, inputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {$_internalSchemaRootDocEq: {a: 1}}}]"; assertPipelineOptimizesTo(inputPipe, inputPipe); @@ -1776,10 +1779,10 @@ TEST(PipelineOptimizationTest, MatchOnInternalSchemaTypeShouldNotSwapSinceCatego "[{$project: {redacted: false}}, " "{$match: {a: {$_internalSchemaType: 1}}}]"; outputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " " {$match: {a: {$_internalSchemaType: [1]}}}]"; serializedPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaType: 1}}}]"; assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); @@ -1809,7 +1812,7 @@ TEST(PipelineOptimizationTest, MatchOnMinLengthShouldSwapWithAdjacentStage) { "{$match: {a: {$_internalSchemaMinLength: 1}}}]"; outputPipe = "[{$match: {a: {$_internalSchemaMinLength: 1}}}," - "{$project: {redacted: false}}]"; + "{$project: {redacted: false, _id: true}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); inputPipe = @@ -1835,7 +1838,7 @@ TEST(PipelineOptimizationTest, MatchOnMaxLengthShouldSwapWithAdjacentStage) { "{$match: {a: {$_internalSchemaMaxLength: 1}}}]"; outputPipe = "[{$match: {a: {$_internalSchemaMaxLength: 1}}}, " - "{$project: {redacted: false}}]"; + "{$project: {redacted: false, _id: true}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); inputPipe = @@ -1857,11 +1860,11 @@ TEST(PipelineOptimizationTest, MatchOnInternalEqShouldSwapWithAdjacentStage) { assertPipelineOptimizesTo(inputPipe, outputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaEq: {c: 1}}}}]"; outputPipe = "[{$match: {a: {$_internalSchemaEq: {c: 1}}}}, " - "{$project: {redacted: false}}]"; + "{$project: {redacted: false, _id: true}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); inputPipe = @@ -1887,10 +1890,10 @@ TEST(PipelineOptimizationTest, MatchOnXorShouldSwapIfEverySubExpressionIsEligibl "{$match: {$_internalSchemaXor: [{a: 1}, {b: 1}]}}]"; outputPipe = "[{$match: {$_internalSchemaXor: [{a: {$eq : 1}}, {b: {$eq : 1}}]}}, " - "{$project: {redacted: false}}]"; + "{$project: {redacted: false, _id: true}}]"; string serializedPipe = "[{$match: {$_internalSchemaXor: [{a: 1}, {b: 1}]}}, " - " {$project: {redacted: false}}]"; + " {$project: {redacted: false, _id: true}}]"; assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); inputPipe = @@ -1926,11 +1929,11 @@ TEST(PipelineOptimizationTest, MatchOnFmodShouldSwapWithAdjacentStage) { assertPipelineOptimizesTo(inputPipe, outputPipe); inputPipe = - "[{$project: {redacted: false}}, " + "[{$project: {redacted: false, _id: true}}, " "{$match: {a: {$_internalSchemaFmod: [5, 0]}}}]"; outputPipe = "[{$match: {a: {$_internalSchemaFmod: [5, 0]}}}, " - "{$project: {redacted: false}}]"; + "{$project: {redacted: false, _id: true}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); inputPipe = @@ -2248,7 +2251,7 @@ TEST(PipelineOptimizationTest, MatchGetsPushedIntoBothChildrenOfUnion) { " coll: 'unionColl'," " pipeline: [" " {$match: {x: {$eq: 2}}}," - " {$project: {y: false}}," + " {$project: {y: false, _id: true}}," " {$sort: {sortKey: {score: 1}}}" " ]" " }}" @@ -2259,7 +2262,7 @@ TEST(PipelineOptimizationTest, MatchGetsPushedIntoBothChildrenOfUnion) { " coll: 'unionColl'," " pipeline: [" " {$match: {x: {$eq: 2}}}," - " {$project: {y: false}}," + " {$project: {y: false, _id: true}}," " {$sort: {score: 1}}" " ]" " }}" @@ -2272,10 +2275,10 @@ TEST(PipelineOptimizationTest, ProjectGetsPushedIntoBothChildrenOfUnion) { " {$unionWith: 'unionColl'}," " {$project: {x: false}}" "]", - "[{$project: {x: false}}," + "[{$project: {x: false, _id: true}}," " {$unionWith: {" " coll: 'unionColl'," - " pipeline: [{$project: {x: false}}]" + " pipeline: [{$project: {x: false, _id: true}}]" " }}]"); // Test an inclusion projection. |