diff options
-rw-r--r-- | jstests/core/explode_for_sort_collation.js | 217 | ||||
-rw-r--r-- | jstests/libs/parallelTester.js | 1 | ||||
-rw-r--r-- | src/mongo/db/query/planner_analysis.cpp | 14 | ||||
-rw-r--r-- | src/mongo/db/query/query_planner_collation_test.cpp | 153 |
4 files changed, 385 insertions, 0 deletions
diff --git a/jstests/core/explode_for_sort_collation.js b/jstests/core/explode_for_sort_collation.js new file mode 100644 index 00000000000..ed81fa990d2 --- /dev/null +++ b/jstests/core/explode_for_sort_collation.js @@ -0,0 +1,217 @@ +/** + * Tests explode for sort query planner behavior with collated queries and indexes. This is a test + * for SERVER-48993. + * @tags: [requires_find_command] + */ +(function() { + "use strict"; + + const testDB = db.getSiblingDB(jsTestName()); + + // Drop the test database. + assert.commandWorked(testDB.dropDatabase()); + + const coll = testDB.explode_for_sort; + + // Executes a test case that creates an index, inserts documents, issues a find query on a + // collection and compares the results with the expected collection. + function executeQueryTestCase(testCase) { + jsTestLog(tojson(testCase)); + + // Create a collection. + testDB.runCommand({drop: coll.getName()}); + assert.commandWorked(testDB.createCollection(coll.getName())); + + // Create an index. + assert.commandWorked(coll.createIndex(testCase.indexKeyPattern, testCase.indexOptions)); + + // Insert some documents into the collection. + assert.writeOK(coll.insert(testCase.inputDocuments)); + + // Run a find query with optionally specified collation. + let cursor = coll.find(testCase.filter).sort(testCase.sort); + if (testCase.findCollation !== undefined) { + cursor = cursor.collation(testCase.findCollation); + } + const actualResults = cursor.toArray(); + + // Compare results to expected. + assert.eq(actualResults, testCase.expectedResults); + } + + const standardInputDocuments = + [{_id: 0, a: 0, b: "CC"}, {_id: 1, a: 0, b: "AA"}, {_id: 2, a: 0, b: "bb"}]; + + const testCases = [ + { + // Verifies that a non-collatable point-query on the prefix of the index key together with + // a + // sort on a suffix of the index key returns correct results when the index is a compound + // index with a non-simple collation and the query does not have an explicit collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: 0}, + sort: {b: 1}, + inputDocuments: standardInputDocuments, + expectedResults: + [{_id: 1, a: 0, b: "AA"}, {_id: 0, a: 0, b: "CC"}, {_id: 2, a: 0, b: "bb"}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key together with + // a + // sort on a suffix of the index key returns correct results when the index is a compound + // index with a non-simple collation and the query explicitly specifies the simple + // collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: 0}, + sort: {b: 1}, + findCollation: {locale: "simple"}, + inputDocuments: standardInputDocuments, + expectedResults: + [{_id: 1, a: 0, b: "AA"}, {_id: 0, a: 0, b: "CC"}, {_id: 2, a: 0, b: "bb"}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key together with + // a + // sort on a suffix of the index key returns correct results when the index is a compound + // index with a simple collation and the query explicitly specifies a non-simple + // collation. + indexKeyPattern: {a: 1, b: 1}, + filter: {a: 0}, + sort: {b: 1}, + findCollation: {locale: "en_US", strength: 1}, + inputDocuments: standardInputDocuments, + expectedResults: + [{_id: 1, a: 0, b: "AA"}, {_id: 2, a: 0, b: "bb"}, {_id: 0, a: 0, b: "CC"}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key together with + // a + // sort on a suffix of the index key returns correct results when the index is a compound + // index with a simple collation and the query explicitly specifies a non-simple + // collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "simple"}}, + filter: {a: 0}, + sort: {b: 1}, + findCollation: {locale: "en_US", strength: 1}, + inputDocuments: standardInputDocuments, + expectedResults: + [{_id: 1, a: 0, b: "AA"}, {_id: 2, a: 0, b: "bb"}, {_id: 0, a: 0, b: "CC"}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key together with + // a + // sort on a suffix of the index key returns correct results when the index is a compound + // index with a non-simple collation that is different from the query's. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 5}}, + filter: {a: 0}, + sort: {b: 1}, + findCollation: {locale: "en_US", strength: 1}, + inputDocuments: standardInputDocuments, + expectedResults: + [{_id: 1, a: 0, b: "AA"}, {_id: 2, a: 0, b: "bb"}, {_id: 0, a: 0, b: "CC"}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key, a collatable + // range-query on the suffix, and a sort on the suffix of the index key returns correct + // results when the index is a compound index with a non-simple collation and the query + // does + // not have an explicit collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: 0, b: {$gte: 'A', $lt: 'D'}}, + sort: {b: 1}, + inputDocuments: standardInputDocuments, + expectedResults: [{_id: 1, a: 0, b: "AA"}, {_id: 0, a: 0, b: "CC"}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key, a collatable + // range-query and a sort on a prefix of a suffix of the index key returns correct results + // when the index is a compound index with a non-simple collation and the query does not + // have an explicit collation. + indexKeyPattern: {a: 1, b: 1, c: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: 0, b: {$gte: 'A', $lt: 'D'}}, + sort: {b: 1}, + inputDocuments: standardInputDocuments, + expectedResults: [{_id: 1, a: 0, b: "AA"}, {_id: 0, a: 0, b: "CC"}] + }, + { + // Verifies that a non-collatable multi-point query on the prefix of the index key, a + // collatable range-query on the suffix, and a sort on the suffix of the index key returns + // correct results when the index is a compound index with a non-simple collation and the + // query does not have an explicit collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: {$in: [0, 2]}, b: {$gte: 'A', $lt: 'D'}}, + sort: {b: 1}, + inputDocuments: [ + {_id: 0, a: 0, b: "CC"}, + {_id: 1, a: 0, b: "AA"}, + {_id: 2, a: 0, b: "bb"}, + {_id: 3, a: 2, b: "BB"} + ], + expectedResults: + [{_id: 1, a: 0, b: "AA"}, {_id: 3, a: 2, b: "BB"}, {_id: 0, a: 0, b: "CC"}] + }, + { + // Verifies that a non-collatable multi-point query on the prefix of the index key, a + // non-collatable range-query on the suffix, and a sort on the suffix of the index key + // returns correct results when the index is a compound index with a non-simple collation + // and the query does not have an explicit collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: {$in: [0, 2]}, b: {$gte: 0, $lt: 10}}, + sort: {b: 1}, + inputDocuments: [ + {_id: 0, a: 0, b: 6}, + {_id: 1, a: 0, b: 10}, + {_id: 2, a: 0, b: "bb"}, + {_id: 3, a: 2, b: 5}, + {_id: 4, a: 2, b: 4} + ], + expectedResults: [{_id: 4, a: 2, b: 4}, {_id: 3, a: 2, b: 5}, {_id: 0, a: 0, b: 6}] + }, + { + // Verifies that a non-collatable multi-point query on the prefix of the index key, a + // non-collatable range-query on the suffix, and a sort on the suffix of the index key + // returns correct results when the index is a compound index with a simple collation + // and the query explicitly specifies a non-simple collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "simple"}}, + filter: {a: {$in: [0, 2]}, b: {$gte: 0, $lt: 10}}, + sort: {b: 1}, + findCollation: {locale: "en_US", strength: 1}, + inputDocuments: [ + {_id: 0, a: 0, b: 6}, + {_id: 1, a: 0, b: 10}, + {_id: 2, a: 0, b: "bb"}, + {_id: 3, a: 2, b: 5}, + {_id: 4, a: 2, b: 4} + ], + expectedResults: [{_id: 4, a: 2, b: 4}, {_id: 3, a: 2, b: 5}, {_id: 0, a: 0, b: 6}] + }, + { + // Verifies that a non-collatable point-query on the prefix of the index key, a + // non-collatable range-query on the suffix, and a sort on the suffix of the index key + // returns correct results when the index is a compound index with a non-simple collation + // and the query does not have an explicit collation. + indexKeyPattern: {a: 1, b: 1}, + indexOptions: {collation: {locale: "en_US", strength: 1}}, + filter: {a: 0, b: {$gte: 0, $lt: 10}}, + sort: {b: 1}, + inputDocuments: [ + {_id: 0, a: 0, b: 6}, + {_id: 1, a: 0, b: 5}, + {_id: 2, a: 0, b: "bb"}, + {_id: 3, a: 0, b: 4} + ], + expectedResults: [{_id: 3, a: 0, b: 4}, {_id: 1, a: 0, b: 5}, {_id: 0, a: 0, b: 6}] + } + ]; + + testCases.forEach(executeQueryTestCase); +}());
\ No newline at end of file diff --git a/jstests/libs/parallelTester.js b/jstests/libs/parallelTester.js index f46f24b76f7..aa6c8bcce75 100644 --- a/jstests/libs/parallelTester.js +++ b/jstests/libs/parallelTester.js @@ -235,6 +235,7 @@ if (typeof _threadInject != "undefined") { // The following tests cannot run when shell readMode is legacy. if (db.getMongo().readMode() === "legacy") { var requires_find_command = [ + "explode_for_sort_collation.js", "views/views_aggregation.js", "views/views_change.js", "views/views_drop.js", diff --git a/src/mongo/db/query/planner_analysis.cpp b/src/mongo/db/query/planner_analysis.cpp index bfe0dc9af47..032f33a6a45 100644 --- a/src/mongo/db/query/planner_analysis.cpp +++ b/src/mongo/db/query/planner_analysis.cpp @@ -438,6 +438,20 @@ bool QueryPlannerAnalysis::explodeForSort(const CanonicalQuery& query, } } + // An index whose collation does not match the query's cannot provide a sort if sort-by + // fields can contain collatable values. + if (!CollatorInterface::collatorsMatch(isn->index.collator, query.getCollator())) { + auto fieldsWithStringBounds = + IndexScanNode::getFieldsWithStringBounds(bounds, isn->index.keyPattern); + for (auto&& element : desiredSort) { + if (fieldsWithStringBounds.count(element.fieldNameStringData()) > 0) { + // The field can contain collatable values and therefore we cannot use the index + // to provide the sort. + return false; + } + } + } + // Do some bookkeeping to see how many ixscans we'll create total. totalNumScans += numScans; diff --git a/src/mongo/db/query/query_planner_collation_test.cpp b/src/mongo/db/query/query_planner_collation_test.cpp index 3f8a88912c0..48bf440cfc8 100644 --- a/src/mongo/db/query/query_planner_collation_test.cpp +++ b/src/mongo/db/query/query_planner_collation_test.cpp @@ -550,6 +550,159 @@ TEST_F(QueryPlannerTest, MustSortInMemoryWhenMinMaxIndexCollationDoesNotMatch) { "{node: {fetch: {node: {ixscan: {pattern: {a: 1, b: 1}}}}}}}}}"); } +// This test verifies that an in-memory sort stage is added and sort provided by an index is not +// used when the collection has a compound index with a non-simple collation and we issue a +// non-collatable point-query on the prefix of the index key together with a sort on a suffix of the +// index key. This is a test for SERVER-48993. +TEST_F(QueryPlannerTest, + MustSortInMemoryWhenPointPrefixQueryHasSimpleCollationButIndexHasNonSimpleCollation) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString); + addIndex(fromjson("{a: 1, b: 1}"), &collator); + + // No explicit collation on the query. This will implicitly use the simple collation since the + // collection does not have a default collation. + runQueryAsCommand(fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}}")); + + assertNumSolutions(2U); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{cscan: {dir: 1}}}}}}"); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{fetch: {node: " + "{ixscan: {pattern: {a: 1, b: 1}}}}}}}}}"); + + // A query with an explicit simple collation. + runQueryAsCommand( + fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}, collation: {locale: 'simple'}}")); + + assertNumSolutions(2U); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{cscan: {dir: 1}}}}}}"); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{fetch: {node: " + "{ixscan: {pattern: {a: 1, b: 1}}}}}}}}}"); +} + +// This test verifies that an in-memory sort stage is added and sort provided by an index is not +// used when the collection has a compound index with a non-specified collation and we issue a +// non-collatable point-query on the prefix of the index key together with a sort on the suffix and +// an explicit non-simple collation. This is a test for SERVER-48993. +TEST_F(QueryPlannerTest, + MustSortInMemoryWhenPointPrefixQueryHasNonSimpleCollationButIndexHasSimpleCollation) { + addIndex(fromjson("{a: 1, b: 1}")); + + runQueryAsCommand( + fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}, collation: {locale: " + "'reverse'}}")); + + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{fetch: {node: " + "{ixscan: {pattern: {a: 1, b: 1}}}}}}}}}"); +} + +// This test verifies that an in-memory sort stage is added and sort provided by indexes is not used +// when the collection has a compound index with a non-simple collation and we issue a +// non-collatable point-query on the prefix of the index key together with a sort on the suffix and +// an explicit non-simple collation that differs from the index collation. This is a test for +// SERVER-48993. +TEST_F(QueryPlannerTest, MustSortInMemoryWhenPointPrefixQueryCollationDoesNotMatchIndexCollation) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString); + addIndex(fromjson("{a: 1, b: 1}"), &collator); + + runQueryAsCommand( + fromjson("{find: 'testns', filter: {a: 2}, sort: {b: 1}, collation: {locale: 'reverse'}}")); + + assertNumSolutions(2U); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{cscan: {dir: 1}}}}}}"); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{fetch: {node: " + "{ixscan: {pattern: {a: 1, b: 1}}}}}}}}}"); +} + +// This test verifies that a sort is provided by an index when the collection has a compound index +// with a non-simple collation and we issue a query with a different non-simple collation is a +// non-collatable point-query on the prefix, a non-collatable range-query on the suffix, and a sort +// on the suffix key. This is a test for SERVER-48993. +TEST_F(QueryPlannerTest, + IndexCanSortWhenPointPrefixQueryCollationDoesNotMatchIndexButSortRangeIsNonCollatable) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString); + addIndex(fromjson("{a: 1, b: 1}"), &collator); + + runQueryAsCommand( + fromjson("{find: 'testns', filter: {a: 2, b: {$gte: 0, $lt: 10}}, sort: {b: 1}}")); + + assertNumSolutions(2U); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{cscan: {dir: 1}}}}}}"); + assertSolutionExists( + "{fetch: {node: " + "{ixscan: {pattern: {a: 1, b: 1}, bounds: {a: [[2, 2, true, true]], b: [[0, 10, true, " + "false]]}}}}}"); +} + +// This test verifies that an in-memory sort stage is added when the collection has a compound index +// with a non-simple collation and we issue a query with a different non-simple collation is a +// non-collatable point-query on the prefix, a collatable range-query on the suffix, and a sort on +// the suffix key. This is a test for SERVER-48993. +TEST_F(QueryPlannerTest, + MustSortInMemoryWhenPointPrefixQueryCollationDoesNotMatchIndexAndSortRangeIsCollatable) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString); + addIndex(fromjson("{a: 1, b: 1}"), &collator); + + runQueryAsCommand( + fromjson("{find: 'testns', filter: {a: 2, b: {$gte: 'B', $lt: 'T'}}, sort: {b: 1}}")); + + assertNumSolutions(2U); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{cscan: {dir: 1}}}}}}"); + assertSolutionExists( + "{sort: {pattern: {b: 1}, limit: 0, node: " + "{sortKeyGen: {node:" + "{fetch: {node: " + "{ixscan: {pattern: {a: 1, b: 1}, bounds: {a: [[2, 2, true, true]]}}}}}}}}}"); +} + +// This test verifies that a SORT_MERGE stage is added when the collection has a compound index with +// a non-simple collation and we issue a query with a different non-simple collation is a +// non-collatable multi-point query on the prefix, a non-collatable range-query on the suffix, and a +// sort on the suffix key.This is a test for SERVER-48993. +TEST_F(QueryPlannerTest, + CanExplodeForSortWhenPointPrefixQueryCollationDoesNotMatchIndexButSortRangeIsNonCollatable) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kToLowerString); + addIndex(fromjson("{a: 1, b: 1, c: 1}"), &collator); + + runQueryAsCommand(fromjson( + "{find: 'testns', filter: {a: {$in: [2, 5]}, b: {$gte: 0, $lt: 10}}, sort: {b: 1}}")); + + assertNumSolutions(2U); + assertSolutionExists( + "{fetch: {node: " + "{mergeSort: {nodes: {" + "n0: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: {a: [[2, 2, true, true]], b: [[0, 10, " + "true, false]]}}}, " + "n1: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: {a: [[5, 5, true, true]], b: [[0, 10, " + "true, false]]}}} " + "}}}}}"); +} + TEST_F(QueryPlannerTest, NoSortStageWhenMinMaxIndexCollationDoesNotMatchButBoundsContainNoStrings) { addIndex(fromjson("{a: 1, b: 1, c: 1}")); |