summaryrefslogtreecommitdiff
path: root/src/mongo
diff options
context:
space:
mode:
authorNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-09-15 16:11:45 -0400
committerNick Zolnierz <nicholas.zolnierz@mongodb.com>2017-09-26 16:46:45 -0400
commitb1e0108555a28fc3f487be642e0d3ac34ddeb794 (patch)
treee4b9be14cc0b9bcd9169f740a25e774897d7cffa /src/mongo
parent78f2325fbf771242457315819df2e5cb341eaeee (diff)
downloadmongo-b1e0108555a28fc3f487be642e0d3ac34ddeb794.tar.gz
SERVER-30987: Enable and test $jsonSchema in the aggregation $match stage
Diffstat (limited to 'src/mongo')
-rw-r--r--src/mongo/db/matcher/schema/expression_internal_schema_root_doc_eq.h5
-rw-r--r--src/mongo/db/matcher/schema/expression_parser_schema_test.cpp16
-rw-r--r--src/mongo/db/pipeline/document_source_match.cpp13
-rw-r--r--src/mongo/db/pipeline/document_source_match_test.cpp101
-rw-r--r--src/mongo/db/pipeline/pipeline.h9
-rw-r--r--src/mongo/db/pipeline/pipeline_d.cpp8
-rw-r--r--src/mongo/db/pipeline/pipeline_test.cpp364
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) {