diff options
Diffstat (limited to 'src/mongo/db')
-rw-r--r-- | src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h | 5 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/expression_parser_schema_test.cpp | 16 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_match.cpp | 13 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_match_test.cpp | 101 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline.h | 9 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline_d.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline_test.cpp | 364 |
7 files changed, 487 insertions, 29 deletions
diff --git a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h index 0255f39236e..cdb69aab699 100644 --- a/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h +++ b/src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h @@ -87,6 +87,11 @@ public: return MatchCategory::kOther; } +protected: + void _doAddDependencies(DepsTracker* deps) const final { + deps->needWholeDocument = true; + } + private: UnorderedFieldsBSONObjComparator _objCmp; BSONObj _rhsObj; diff --git a/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp index 154e395a125..ac6fb95f42b 100644 --- a/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp +++ b/src/mongo/db/matcher/schema/expression_parser_schema_test.cpp @@ -516,28 +516,28 @@ TEST(MatchExpressionParserSchemaTest, MatchArrayIndexParsesSuccessfully) { TEST(InternalSchemaAllElemMatchFromIndexMatchExpression, FailsToParseWithNegativeIndex) { BSONObj matchPredicate = - fromjson("{$_internalSchemaAllElemMatchFromIndex: [-2, {a: { $lt: 0 }}]}"); + fromjson("{a: {$_internalSchemaAllElemMatchFromIndex: [-2, {a: { $lt: 0 }}]}}"); auto expr = MatchExpressionParser::parse(matchPredicate, kSimpleCollator); - ASSERT_NOT_OK(expr.getStatus()); + ASSERT_EQ(expr.getStatus(), ErrorCodes::TypeMismatch); } TEST(InternalSchemaAllElemMatchFromIndexMatchExpression, FailsToParseWithNonObjectExpression) { - BSONObj matchPredicate = fromjson("{$_internalSchemaAllElemMatchFromIndex: [-2, 4]}"); + BSONObj matchPredicate = fromjson("{a: {$_internalSchemaAllElemMatchFromIndex: [-2, 4]}}"); auto expr = MatchExpressionParser::parse(matchPredicate, kSimpleCollator); - ASSERT_NOT_OK(expr.getStatus()); + ASSERT_EQ(expr.getStatus(), ErrorCodes::TypeMismatch); } TEST(InternalSchemaAllElemMatchFromIndexMatchExpression, FailsToParseWithInvalidExpression) { BSONObj matchPredicate = - fromjson("{$_internalSchemaAllElemMatchFromIndex: [-2, {$fakeExpression: 4}]}"); + fromjson("{a: {$_internalSchemaAllElemMatchFromIndex: [-2, {$fakeExpression: 4}]}}"); auto expr = MatchExpressionParser::parse(matchPredicate, kSimpleCollator); - ASSERT_NOT_OK(expr.getStatus()); + ASSERT_EQ(expr.getStatus(), ErrorCodes::TypeMismatch); } TEST(InternalSchemaAllElemMatchFromIndexMatchExpression, FailsToParseWithEmptyArray) { - BSONObj matchPredicate = fromjson("{$_internalSchemaAllElemMatchFromIndex: []}"); + BSONObj matchPredicate = fromjson("{a: {$_internalSchemaAllElemMatchFromIndex: []}}"); auto expr = MatchExpressionParser::parse(matchPredicate, kSimpleCollator); - ASSERT_NOT_OK(expr.getStatus()); + ASSERT_EQ(expr.getStatus(), ErrorCodes::FailedToParse); } TEST(InternalSchemaAllElemMatchFromIndexMatchExpression, ParsesCorreclyWithValidInput) { diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index f50faf85749..5a954c63358 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -499,13 +499,12 @@ DocumentSourceMatch::DocumentSourceMatch(const BSONObj& query, _isTextQuery(isTextQuery(query)), _dependencies(_isTextQuery ? DepsTracker::MetadataAvailable::kTextScore : DepsTracker::MetadataAvailable::kNoMetadata) { - StatusWithMatchExpression status = uassertStatusOK( - MatchExpressionParser::parse(_predicate, - pExpCtx->getCollator(), - pExpCtx, - ExtensionsCallbackNoop(), - MatchExpressionParser::AllowedFeatures::kText | - MatchExpressionParser::AllowedFeatures::kExpr)); + StatusWithMatchExpression status = + uassertStatusOK(MatchExpressionParser::parse(_predicate, + pExpCtx->getCollator(), + pExpCtx, + ExtensionsCallbackNoop(), + Pipeline::kAllowedMatcherFeatures)); _expression = std::move(status.getValue()); getDependencies(&_dependencies); diff --git a/src/mongo/db/pipeline/document_source_match_test.cpp b/src/mongo/db/pipeline/document_source_match_test.cpp index c1e004d9e26..b327f811cd1 100644 --- a/src/mongo/db/pipeline/document_source_match_test.cpp +++ b/src/mongo/db/pipeline/document_source_match_test.cpp @@ -307,6 +307,87 @@ TEST_F(DocumentSourceMatchTest, ASSERT_EQUALS(false, dependencies.getNeedTextScore()); } +TEST_F(DocumentSourceMatchTest, + ShouldAddWholeDocumentAsDependencyOfClausesWithInternalSchemaRootDocEq) { + auto query = fromjson("{$_internalSchemaRootDocEq: {a: 1}}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + DepsTracker dependencies; + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(0U, dependencies.fields.size()); + ASSERT_EQUALS(true, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + +TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithInternalSchemaType) { + auto query = fromjson("{a: {$_internalSchemaType: 1}}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + DepsTracker dependencies; + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(1U, dependencies.fields.size()); + ASSERT_EQUALS(1U, dependencies.fields.count("a")); + ASSERT_EQUALS(false, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + +TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithInternalSchemaCond) { + auto query = fromjson("{$_internalSchemaCond: [{a: 1}, {b: 1}, {c: 1}]}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + DepsTracker dependencies; + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(3U, dependencies.fields.size()); + ASSERT_EQUALS(1U, dependencies.fields.count("a")); + ASSERT_EQUALS(1U, dependencies.fields.count("b")); + ASSERT_EQUALS(1U, dependencies.fields.count("c")); + ASSERT_EQUALS(false, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + +TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithInternalSchemaXor) { + auto query = fromjson("{$_internalSchemaXor: [{a: 1}, {b: 1}, {c: 1}]}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + DepsTracker dependencies; + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(3U, dependencies.fields.size()); + ASSERT_EQUALS(1U, dependencies.fields.count("a")); + ASSERT_EQUALS(1U, dependencies.fields.count("b")); + ASSERT_EQUALS(1U, dependencies.fields.count("c")); + ASSERT_EQUALS(false, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + +TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithEmptyJSONSchema) { + DepsTracker dependencies; + auto query = fromjson("{$jsonSchema: {}}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(0U, dependencies.fields.size()); + ASSERT_EQUALS(false, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + +TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForClausesWithJSONSchemaProperties) { + DepsTracker dependencies; + auto query = fromjson("{$jsonSchema: {properties: {a: {type: 'number'}}}}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(1U, dependencies.fields.count("a")); + ASSERT_EQUALS(1U, dependencies.fields.size()); + ASSERT_EQUALS(false, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + +TEST_F(DocumentSourceMatchTest, ShouldAddCorrectDependenciesForMultiplePredicatesWithJSONSchema) { + DepsTracker dependencies; + auto query = fromjson("{$jsonSchema: {properties: {a: {type: 'number'}}}, b: 1}"); + auto match = DocumentSourceMatch::create(query, getExpCtx()); + ASSERT_EQUALS(DocumentSource::SEE_NEXT, match->getDependencies(&dependencies)); + ASSERT_EQUALS(2U, dependencies.fields.size()); + ASSERT_EQUALS(1U, dependencies.fields.count("a")); + ASSERT_EQUALS(1U, dependencies.fields.count("b")); + ASSERT_EQUALS(false, dependencies.needWholeDocument); + ASSERT_EQUALS(false, dependencies.getNeedTextScore()); +} + TEST_F(DocumentSourceMatchTest, ShouldAddOuterFieldToDependenciesIfElemMatchContainsNoFieldNames) { auto match = DocumentSourceMatch::create(fromjson("{a: {$elemMatch: {$gt: 1, $lt: 5}}}"), getExpCtx()); @@ -516,5 +597,25 @@ TEST_F(DocumentSourceMatchTest, ShouldCorrectlyEvaluateElemMatchPredicate) { ASSERT_TRUE(match->getNext().isEOF()); } +TEST_F(DocumentSourceMatchTest, ShouldCorrectlyEvaluateJSONSchemaPredicate) { + const auto match = DocumentSourceMatch::create( + fromjson("{$jsonSchema: {properties: {a: {type: 'number'}}}}"), getExpCtx()); + + const auto mock = DocumentSourceMock::create( + {Document{{"a", 1}}, Document{{"a", "str"_sd}}, Document{{"a", {Document{{0, 1}}}}}}); + + match->setSource(mock.get()); + + // The first result should match. + auto next = match->getNext(); + ASSERT_TRUE(next.isAdvanced()); + ASSERT_DOCUMENT_EQ(next.releaseDocument(), (Document{{"a", 1}})); + + // The rest should not match. + ASSERT_TRUE(match->getNext().isEOF()); + ASSERT_TRUE(match->getNext().isEOF()); + ASSERT_TRUE(match->getNext().isEOF()); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h index c5e94837019..0aa142c36c8 100644 --- a/src/mongo/db/pipeline/pipeline.h +++ b/src/mongo/db/pipeline/pipeline.h @@ -33,6 +33,7 @@ #include <boost/intrusive_ptr.hpp> +#include "mongo/db/matcher/expression_parser.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/dependencies.h" #include "mongo/db/pipeline/value.h" @@ -97,6 +98,14 @@ public: }; /** + * List of supported match expression features in a pipeline. + */ + static constexpr MatchExpressionParser::AllowedFeatureSet kAllowedMatcherFeatures = + MatchExpressionParser::AllowedFeatures::kText | + MatchExpressionParser::AllowedFeatures::kExpr | + MatchExpressionParser::AllowedFeatures::kJSONSchema; + + /** * Parses a Pipeline from a vector of BSONObjs. Returns a non-OK status if it failed to parse. * The returned pipeline is not optimized, but the caller may convert it to an optimized * pipeline by calling optimizePipeline(). diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp index 8f15e12e8bb..c6642606c09 100644 --- a/src/mongo/db/pipeline/pipeline_d.cpp +++ b/src/mongo/db/pipeline/pipeline_d.cpp @@ -451,12 +451,8 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> attemptToGetExe const ExtensionsCallbackReal extensionsCallback(pExpCtx->opCtx, &nss); - auto cq = CanonicalQuery::canonicalize(opCtx, - std::move(qr), - pExpCtx, - extensionsCallback, - MatchExpressionParser::AllowedFeatures::kText | - MatchExpressionParser::AllowedFeatures::kExpr); + auto cq = CanonicalQuery::canonicalize( + opCtx, std::move(qr), pExpCtx, extensionsCallback, Pipeline::kAllowedMatcherFeatures); if (!cq.isOK()) { // Return an error instead of uasserting, since there are cases where the combination of diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp index b589173f748..e84f30dca74 100644 --- a/src/mongo/db/pipeline/pipeline_test.cpp +++ b/src/mongo/db/pipeline/pipeline_test.cpp @@ -959,21 +959,254 @@ TEST(PipelineOptimizationTest, MatchCannotSwapWithSortLimit) { assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, inputPipe); } -TEST(PipelineOptimizationTest, MatchOnMinItemsShouldNotMoveAcrossRename) { - string pipeline = +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnMinItemsShouldNotSwapSinceCategoryIsArrayMatching) { + string inputPipe = "[{$project: {_id: true, a: '$b'}}, " "{$match: {a: {$_internalSchemaMinItems: 1}}}]"; - assertPipelineOptimizesTo(pipeline, pipeline); + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaMinItems: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaMinItems: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); } -TEST(PipelineOptimizationTest, MatchOnMaxItemsShouldNotMoveAcrossRename) { - string pipeline = +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnMaxItemsShouldNotSwapSinceCategoryIsArrayMatching) { + string inputPipe = "[{$project: {_id: true, a: '$b'}}, " "{$match: {a: {$_internalSchemaMaxItems: 1}}}]"; - assertPipelineOptimizesTo(pipeline, pipeline); + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaMaxItems: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaMaxItems: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, + MatchOnAllElemMatchFromIndexShouldNotSwapSinceCategoryIsArrayMatching) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {a: {$_internalSchemaAllElemMatchFromIndex: [1, {b: {$gt: 0}}]}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaAllElemMatchFromIndex: [1, {b: {$gt: 0}}]}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {a: {$_internalSchemaAllElemMatchFromIndex: [1, {b: {$gt: 0}}]}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnArrayIndexShouldNotSwapSinceCategoryIsArrayMatching) { + string inputPipe = R"( + [{$project: {_id: true, a: '$b'}}, + {$match: {a: {$_internalSchemaMatchArrayIndex: + {index: 0, namePlaceholder: 'i', expression: {i: {$lt: 0}}}}}}])"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = R"( + [{$project: {redacted: false}}, + {$match: {a: {$_internalSchemaMatchArrayIndex: + {index: 0, namePlaceholder: 'i', expression: {i: {$lt: 0}}}}}}])"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = R"( + [{$addFields : {a : {$const: 1}}}, + {$match: {a: {$_internalSchemaMatchArrayIndex: + {index: 0, namePlaceholder: 'i', expression: {i: {$lt: 0}}}}}}])"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnUniqueItemsShouldNotSwapSinceCategoryIsArrayMatching) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {a: {$_internalSchemaUniqueItems: true}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaUniqueItems: true}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {a: {$_internalSchemaUniqueItems: true}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnObjectMatchShouldNotSwapSinceCategoryIsOther) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {a: {$_internalSchemaObjectMatch: {b: 1}}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaObjectMatch: {b: 1}}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {a: {$_internalSchemaObjectMatch: {b: 1}}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnMinPropertiesShouldNotSwapSinceCategoryIsOther) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {$_internalSchemaMinProperties: 2}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {$_internalSchemaMinProperties: 2}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {$_internalSchemaMinProperties: 2}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnMaxPropertiesShouldNotSwapSinceCategoryIsOther) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {$_internalSchemaMaxProperties: 2}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {$_internalSchemaMaxProperties: 2}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {$_internalSchemaMaxProperties: 2}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnAllowedPropertiesShouldNotSwapSinceCategoryIsOther) { + string inputPipe = R"( + [{$project: {_id: true, a: '$b'}}, + {$match: {$_internalSchemaAllowedProperties: { + properties: ['b'], + namePlaceholder: 'i', + patternProperties: [], + otherwise: {i: 1} + }}}])"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = R"( + [{$project: {redacted: false}}, + {$match: {$_internalSchemaAllowedProperties: { + properties: ['b'], + namePlaceholder: 'i', + patternProperties: [], + otherwise: {i: 1} + }}}])"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = R"( + [{$addFields : {a : {$const: 1}}}, + {$match: {$_internalSchemaAllowedProperties: { + properties: ['b'], + namePlaceholder: 'i', + patternProperties: [], + otherwise: {i: 1} + }}}])"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnCondShouldNotSwapSinceCategoryIsOther) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {$_internalSchemaCond: [{a: 1}, {b: 1}, {c: 1}]}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {$_internalSchemaCond: [{a: 1}, {b: 1}, {c: 1}]}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {$_internalSchemaCond: [{a: 1}, {b: 1}, {c: 1}]}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnRootDocEqShouldNotSwapSinceCategoryIsOther) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {$_internalSchemaRootDocEq: {a: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {$_internalSchemaRootDocEq: {a: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {$_internalSchemaRootDocEq: {a: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); } -TEST(PipelineOptimizationTest, MatchOnMinLengthShouldMoveAcrossRename) { +// Descriptive test. The following internal match expression *could* participate in pipeline +// optimizations, but it currently does not. +TEST(PipelineOptimizationTest, MatchOnInternalSchemaTypeShouldNotSwapSinceCategoryIsOther) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {a: {$_internalSchemaType: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaType: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaType: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +TEST(PipelineOptimizationTest, MatchOnMinLengthShouldSwapWithAdjacentStage) { string inputPipe = "[{$project: {_id: true, a: '$b'}}, " "{$match: {a: {$_internalSchemaMinLength: 1}}}]"; @@ -981,9 +1214,25 @@ TEST(PipelineOptimizationTest, MatchOnMinLengthShouldMoveAcrossRename) { "[{$match: {b: {$_internalSchemaMinLength: 1}}}," "{$project: {_id: true, a: '$b'}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaMinLength: 1}}}]"; + outputPipe = + "[{$match: {a: {$_internalSchemaMinLength: 1}}}," + "{$project: {redacted: false}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaMinLength: 1}}}]"; + outputPipe = + "[{$match: {b: {$_internalSchemaMinLength: 1}}}," + "{$addFields: {a: {$const: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); } -TEST(PipelineOptimizationTest, MatchOnMaxLengthShouldMoveAcrossRename) { +TEST(PipelineOptimizationTest, MatchOnMaxLengthShouldSwapWithAdjacentStage) { string inputPipe = "[{$project: {_id: true, a: '$b'}}, " "{$match: {a: {$_internalSchemaMaxLength: 1}}}]"; @@ -991,6 +1240,105 @@ TEST(PipelineOptimizationTest, MatchOnMaxLengthShouldMoveAcrossRename) { "[{$match: {b: {$_internalSchemaMaxLength: 1}}}," "{$project: {_id: true, a: '$b'}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaMaxLength: 1}}}]"; + outputPipe = + "[{$match: {a: {$_internalSchemaMaxLength: 1}}}, " + "{$project: {redacted: false}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaMaxLength: 1}}}]"; + outputPipe = + "[{$match: {b: {$_internalSchemaMaxLength: 1}}}, " + "{$addFields: {a: {$const: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); +} + +TEST(PipelineOptimizationTest, MatchOnInternalEqShouldSwapWithAdjacentStage) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {a: {$_internalSchemaEq: {c: 1}}}}]"; + string outputPipe = + "[{$match: {b: {$_internalSchemaEq: {c: 1}}}}, " + "{$project: {_id: true, a: '$b'}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaEq: {c: 1}}}}]"; + outputPipe = + "[{$match: {a: {$_internalSchemaEq: {c: 1}}}}, " + "{$project: {redacted: false}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaEq: {c: 1}}}}]"; + outputPipe = + "[{$match: {b: {$_internalSchemaEq: {c: 1}}}}, " + "{$addFields: {a: {$const: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); +} + +TEST(PipelineOptimizationTest, MatchOnXorShouldSwapIfEverySubExpressionIsEligible) { + string inputPipe = + "[{$project: {_id: true, a: '$b', c: '$d'}}, " + "{$match: {$_internalSchemaXor: [{a: 1}, {c: 1}]}}]"; + string outputPipe = + "[{$match: {$_internalSchemaXor: [{b: {$eq: 1}}, {d: {$eq: 1}}]}}, " + "{$project: {_id: true, a: '$b', c: '$d'}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {$_internalSchemaXor: [{a: 1}, {b: 1}]}}]"; + outputPipe = + "[{$match: {$_internalSchemaXor: [{a: 1}, {b: 1}]}}, " + "{$project: {redacted: false}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {$_internalSchemaXor: [{b: 1}, {c: 1}]}}]"; + outputPipe = + "[{$match: {$_internalSchemaXor: [{b: 1}, {c: 1}]}}, " + "{$addFields: {a: {$const: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {$_internalSchemaXor: [{b: 1}, {a: 1}]}}]"; + assertPipelineOptimizesTo(inputPipe, inputPipe); +} + +TEST(PipelineOptimizationTest, MatchOnFmodShouldSwapWithAdjacentStage) { + string inputPipe = + "[{$project: {_id: true, a: '$b'}}, " + "{$match: {a: {$_internalSchemaFmod: [5, 0]}}}]"; + string outputPipe = + "[{$match: {b: {$_internalSchemaFmod: [5, 0]}}}, " + "{$project: {_id: true, a: '$b'}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$project: {redacted: false}}, " + "{$match: {a: {$_internalSchemaFmod: [5, 0]}}}]"; + outputPipe = + "[{$match: {a: {$_internalSchemaFmod: [5, 0]}}}, " + "{$project: {redacted: false}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); + + inputPipe = + "[{$addFields : {a : {$const: 1}}}, " + "{$match: {b: {$_internalSchemaFmod: [5, 0]}}}]"; + outputPipe = + "[{$match: {b: {$_internalSchemaFmod: [5, 0]}}}, " + "{$addFields: {a: {$const: 1}}}]"; + assertPipelineOptimizesTo(inputPipe, outputPipe); } TEST(PipelineOptimizationTest, ChangeStreamLookupSwapsWithIndependentMatch) { |