diff options
author | Arun Banala <arun.banala@mongodb.com> | 2022-12-28 13:32:46 -0800 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2023-01-05 00:01:29 +0000 |
commit | 2f1b039897aa55eb3007abbe1020e6e4967b4140 (patch) | |
tree | d1927877b103a38a77b43ad172137eaa23e1d102 | |
parent | 67c5ecd1847fe92b871ef9e26d3fb35f7bf1d662 (diff) | |
download | mongo-2f1b039897aa55eb3007abbe1020e6e4967b4140.tar.gz |
SERVER-72416 Find and findAndModify commands' ExpressionContext should inherit collection level collation
21 files changed, 338 insertions, 59 deletions
diff --git a/buildscripts/resmokeconfig/matrix_suites/overrides/replica_sets_stepdown_selector.yml b/buildscripts/resmokeconfig/matrix_suites/overrides/replica_sets_stepdown_selector.yml index 29cfad55d50..04cc8d5fe2e 100644 --- a/buildscripts/resmokeconfig/matrix_suites/overrides/replica_sets_stepdown_selector.yml +++ b/buildscripts/resmokeconfig/matrix_suites/overrides/replica_sets_stepdown_selector.yml @@ -21,6 +21,7 @@ - jstests/core/find_and_modify.js - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js @@ -82,6 +83,7 @@ - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_pipeline_update.js - jstests/core/find_and_modify_server6865.js + - jstests/core/project_with_collation.js # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml index 92bf6acf8d3..975be8f9727 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml @@ -222,6 +222,7 @@ selector: - jstests/core/find_and_modify.js - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js + - jstests/core/project_with_collation.js # Does not support tojson of command objects. - jstests/core/SERVER-23626.js diff --git a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml index f0187167f88..b37bed5442c 100644 --- a/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml @@ -30,6 +30,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_pipeline_update.js - jstests/core/find_and_modify_server6865.js + - jstests/core/project_with_collation.js # These test run commands using legacy queries, which are not supported on sessions. - jstests/core/comment_field.js diff --git a/buildscripts/resmokeconfig/suites/retryable_writes_downgrade.yml b/buildscripts/resmokeconfig/suites/retryable_writes_downgrade.yml index 1185ffc9001..1cb40f8e8ff 100644 --- a/buildscripts/resmokeconfig/suites/retryable_writes_downgrade.yml +++ b/buildscripts/resmokeconfig/suites/retryable_writes_downgrade.yml @@ -23,6 +23,7 @@ selector: - jstests/core/find_and_modify.js - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml index 38f02a9e37d..d7851ee93c1 100644 --- a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml @@ -29,6 +29,7 @@ selector: - jstests/core/find_and_modify_pipeline_update.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # These tests rely on the assumption that an update command is run only once. - jstests/core/find_and_modify_metrics.js diff --git a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml index a3ba66bc673..017cc313537 100644 --- a/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml @@ -22,6 +22,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/shard_split_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/shard_split_kill_primary_jscore_passthrough.yml index b8e24aa6921..a70e99a615f 100644 --- a/buildscripts/resmokeconfig/suites/shard_split_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/shard_split_kill_primary_jscore_passthrough.yml @@ -43,6 +43,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/shard_split_stepdown_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/shard_split_stepdown_jscore_passthrough.yml index 7009859f454..cbf2ada3128 100644 --- a/buildscripts/resmokeconfig/suites/shard_split_stepdown_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/shard_split_stepdown_jscore_passthrough.yml @@ -43,6 +43,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/shard_split_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/shard_split_terminate_primary_jscore_passthrough.yml index b324978c959..277f69e2470 100644 --- a/buildscripts/resmokeconfig/suites/shard_split_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/shard_split_terminate_primary_jscore_passthrough.yml @@ -43,6 +43,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/sharded_retryable_writes_downgrade.yml b/buildscripts/resmokeconfig/suites/sharded_retryable_writes_downgrade.yml index 22015b9a7f0..0b2a40c9cb1 100644 --- a/buildscripts/resmokeconfig/suites/sharded_retryable_writes_downgrade.yml +++ b/buildscripts/resmokeconfig/suites/sharded_retryable_writes_downgrade.yml @@ -23,6 +23,7 @@ selector: - jstests/core/find_and_modify.js - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml index 45dba58bf3c..5c48ad527c7 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml @@ -42,6 +42,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml index f3946ff0211..a080520293b 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml @@ -41,6 +41,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml b/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml index 2aa00ffca65..1b6577beb2e 100644 --- a/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml +++ b/buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml @@ -41,6 +41,7 @@ selector: - jstests/core/find_and_modify2.js - jstests/core/find_and_modify_server6865.js - jstests/core/fts_find_and_modify.js + - jstests/core/project_with_collation.js # Stepdown commands during fsync lock will fail. - jstests/core/currentop.js diff --git a/etc/backports_required_for_multiversion_tests.yml b/etc/backports_required_for_multiversion_tests.yml index 8819f0e52bd..9172c8251a6 100644 --- a/etc/backports_required_for_multiversion_tests.yml +++ b/etc/backports_required_for_multiversion_tests.yml @@ -292,6 +292,12 @@ last-continuous: ticket: SERVER-71689 - test_file: jstests/sharding/hidden_index.js ticket: SERVER-71638 + - test_file: jstests/core/project_with_collation.js + ticket: SERVER-72416 + - test_file: jstests/core/collation.js + ticket: SERVER-72416 + - test_file: jstests/core/or_to_in.js + ticket: SERVER-72416 suites: null last-lts: all: @@ -659,4 +665,10 @@ last-lts: ticket: SERVER-71689 - test_file: jstests/sharding/hidden_index.js ticket: SERVER-71638 + - test_file: jstests/core/project_with_collation.js + ticket: SERVER-72416 + - test_file: jstests/core/collation.js + ticket: SERVER-72416 + - test_file: jstests/core/or_to_in.js + ticket: SERVER-72416 suites: null diff --git a/jstests/core/collation.js b/jstests/core/collation.js index 12843b40864..1bbdc2cd47a 100644 --- a/jstests/core/collation.js +++ b/jstests/core/collation.js @@ -27,7 +27,6 @@ load("jstests/libs/index_catalog_helpers.js"); load("jstests/concurrency/fsm_workload_helpers/server_types.js"); // For isReplSet load("jstests/libs/fixture_helpers.js"); -load("jstests/libs/sbe_explain_helpers.js"); // For engineSpecificAssertion. // For areAllCollectionsClustered. load("jstests/libs/clustered_collections/clustered_collection_util.js"); @@ -758,9 +757,7 @@ if (!isClustered) { assert.commandWorked(testDb.createCollection(coll.getName(), {collation: {locale: "en_US"}})); explainRes = coll.explain("executionStats").find({_id: "foo"}).finish(); assert.commandWorked(explainRes); - let classicAssert = null !== getPlanStage(getWinningPlan(explainRes.queryPlanner), "IDHACK"); - let sbeAssert = null !== getPlanStage(getWinningPlan(explainRes.queryPlanner), "IXSCAN"); - engineSpecificAssertion(classicAssert, sbeAssert, testDb, explainRes); + assert.neq(null, getPlanStage(explainRes.executionStats.executionStages, "IDHACK"), explainRes); // Find on _id should use idhack stage when explicitly given query collation matches // collection default. diff --git a/jstests/core/or_to_in.js b/jstests/core/or_to_in.js index 3c9cacfbcdf..5d3c745dc95 100644 --- a/jstests/core/or_to_in.js +++ b/jstests/core/or_to_in.js @@ -25,19 +25,25 @@ function compareValues(v1, v2) { } // Check that 'expectedQuery' and 'actualQuery' have the same plans, and produce the same result. -function assertEquivPlanAndResult(expectedQuery, actualQuery) { +function assertEquivPlanAndResult(expectedQuery, actualQuery, supportWithCollation) { const expectedExplain = coll.find(expectedQuery).explain("queryPlanner"); const actualExplain = coll.find(actualQuery).explain("queryPlanner"); // The queries must be rewritten into the same form. - assert.docEq(expectedExplain.parsedQuery, actualExplain.parsedQuery); + assert.docEq(expectedExplain.queryPlanner.parsedQuery, actualExplain.queryPlanner.parsedQuery); - // Check if the test queries produce the same plans with collations - const expectedExplainColln = + // We are always running these queries to ensure a server crash is not triggered. + // TODO SERVER-72450: Add appropriate assertions for the output. + const expectedExplainCollation = coll.find(expectedQuery).sort({f1: 1}).collation({locale: 'en_US'}).explain("queryPlanner"); - const actualExplainColln = + const actualExplainCollation = coll.find(actualQuery).sort({f1: 1}).collation({locale: 'en_US'}).explain("queryPlanner"); - assert.docEq(expectedExplainColln.parsedQuery, actualExplainColln.parsedQuery); + + if (supportWithCollation) { + // Check if the test queries produce the same plans with collations. + assert.docEq(expectedExplainCollation.queryPlanner.parsedQuery, + actualExplainCollation.queryPlanner.parsedQuery); + } // Make sure both queries have the same access plan. const expectedPlan = getWinningPlan(expectedExplain.queryPlanner); @@ -51,12 +57,13 @@ function assertEquivPlanAndResult(expectedQuery, actualQuery) { const actualRes = coll.find(actualQuery).toArray(); assert(arrayEq(expectedRes, actualRes, false, compareValues), `expected=${expectedRes}, actual=${actualRes}`); + // also with collation - const expectedResColln = + const expectedResCollation = coll.find(expectedQuery).sort({f1: 1}).collation({locale: 'en_US'}).toArray(); - const actualResColln = + const actualResCollation = coll.find(actualQuery).sort({f1: 1}).collation({locale: 'en_US'}).toArray(); - assert(arrayEq(expectedResColln, actualResColln, false, compareValues), + assert(arrayEq(expectedResCollation, actualResCollation, false, compareValues), `expected=${expectedRes}, actual=${actualRes}`); } @@ -94,32 +101,71 @@ assert.commandWorked(coll.insert(data)); // Pairs of queries where the first one is expressed via OR (which is supposed to be // rewritten as IN), and the second one is an equivalent query using IN. +// +// The third element of the array is optional, if present, implies that the rewrite is not +// supported when there is a collation involved. +// +// TODO SERVER-72450: Remove or update this logic related to collation, and enforce stronger +// assertions. const positiveTestQueries = [ - [{$or: [{f1: 5}, {f1: 3}, {f1: 7}]}, {f1: {$in: [7, 3, 5]}}], - [{$or: [{f1: {$eq: 5}}, {f1: {$eq: 3}}, {f1: {$eq: 7}}]}, {f1: {$in: [7, 3, 5]}}], - [{$or: [{f1: 42}, {f1: NaN}, {f1: 99}]}, {f1: {$in: [42, NaN, 99]}}], - [{$or: [{f1: /^x/}, {f1: "ab"}]}, {f1: {$in: [/^x/, "ab"]}}], - [{$or: [{f1: /^x/}, {f1: "^a"}]}, {f1: {$in: [/^x/, "^a"]}}], - [{$or: [{f1: 42}, {f1: null}, {f1: 99}]}, {f1: {$in: [42, 99, null]}}], - [{$or: [{f1: 1}, {f2: 9}, {f1: 99}]}, {$or: [{f2: 9}, {f1: {$in: [1, 99]}}]}], - [{$or: [{f1: {$regex: /^x/}}, {f1: {$regex: /ab/}}]}, {f1: {$in: [/^x/, /ab/]}}], - [ - {$and: [{$or: [{f1: 7}, {f1: 3}, {f1: 5}]}, {$or: [{f1: 1}, {f1: 2}, {f1: 3}]}]}, - {$and: [{f1: {$in: [7, 3, 5]}}, {f1: {$in: [1, 2, 3]}}]} - ], - [ - {$or: [{$or: [{f1: 7}, {f1: 3}, {f1: 5}]}, {$or: [{f1: 1}, {f1: 2}, {f1: 3}]}]}, - {$or: [{f1: {$in: [7, 3, 5]}}, {f1: {$in: [1, 2, 3]}}]} - ], - [ - {$or: [{$and: [{f1: 7}, {f2: 7}, {f1: 5}]}, {$or: [{f1: 1}, {f1: 2}, {f1: 3}]}]}, - {$or: [{$and: [{f1: 7}, {f2: 7}, {f1: 5}]}, {f1: {$in: [1, 2, 3]}}]}, - ], - [{$or: [{f2: [32, 52]}, {f2: [42, [13, 11]]}]}, {f2: {$in: [[32, 52], [42, [13, 11]]]}}], - [{$or: [{f2: 52}, {f2: 13}]}, {f2: {$in: [52, 13]}}], - [{$or: [{f2: [11]}, {f2: [23]}]}, {f2: {$in: [[11], [23]]}}], - [{$or: [{f1: 42}, {f1: null}]}, {f1: {$in: [42, null]}}], - [{$or: [{f1: "a"}, {f1: "b"}, {f1: /c/}]}, {f1: {$in: ["a", "b", /c/]}}], + {actualQuery: {$or: [{f1: 5}, {f1: 3}, {f1: 7}]}, expectedQuery: {f1: {$in: [7, 3, 5]}}}, + { + actualQuery: {$or: [{f1: {$eq: 5}}, {f1: {$eq: 3}}, {f1: {$eq: 7}}]}, + expectedQuery: {f1: {$in: [7, 3, 5]}} + }, + { + actualQuery: {$or: [{f1: 42}, {f1: NaN}, {f1: 99}]}, + expectedQuery: {f1: {$in: [42, NaN, 99]}} + }, + { + actualQuery: {$or: [{f1: /^x/}, {f1: "ab"}]}, + expectedQuery: {f1: {$in: [/^x/, "ab"]}}, + cannotRewriteWithCollation: true + }, + { + actualQuery: {$or: [{f1: /^x/}, {f1: "^a"}]}, + expectedQuery: {f1: {$in: [/^x/, "^a"]}}, + cannotRewriteWithCollation: true + }, + { + actualQuery: {$or: [{f1: 42}, {f1: null}, {f1: 99}]}, + expectedQuery: {f1: {$in: [42, 99, null]}} + }, + { + actualQuery: {$or: [{f1: 1}, {f2: 9}, {f1: 99}]}, + expectedQuery: {$or: [{f2: 9}, {f1: {$in: [1, 99]}}]} + }, + { + actualQuery: {$or: [{f1: {$regex: /^x/}}, {f1: {$regex: /ab/}}]}, + expectedQuery: {f1: {$in: [/^x/, /ab/]}} + }, + { + actualQuery: + {$and: [{$or: [{f1: 7}, {f1: 3}, {f1: 5}]}, {$or: [{f1: 1}, {f1: 2}, {f1: 3}]}]}, + expectedQuery: {$and: [{f1: {$in: [7, 3, 5]}}, {f1: {$in: [1, 2, 3]}}]} + }, + { + actualQuery: + {$or: [{$or: [{f1: 7}, {f1: 3}, {f1: 5}]}, {$or: [{f1: 1}, {f1: 2}, {f1: 3}]}]}, + expectedQuery: {$or: [{f1: {$in: [7, 3, 5]}}, {f1: {$in: [1, 2, 3]}}]} + }, + { + actualQuery: + {$or: [{$and: [{f1: 7}, {f2: 7}, {f1: 5}]}, {$or: [{f1: 1}, {f1: 2}, {f1: 3}]}]}, + expectedQuery: {$or: [{$and: [{f1: 7}, {f2: 7}, {f1: 5}]}, {f1: {$in: [1, 2, 3]}}]}, + }, + { + actualQuery: {$or: [{f2: [32, 52]}, {f2: [42, [13, 11]]}]}, + expectedQuery: {f2: {$in: [[32, 52], [42, [13, 11]]]}} + }, + {actualQuery: {$or: [{f2: 52}, {f2: 13}]}, expectedQuery: {f2: {$in: [52, 13]}}}, + {actualQuery: {$or: [{f2: [11]}, {f2: [23]}]}, expectedQuery: {f2: {$in: [[11], [23]]}}}, + {actualQuery: {$or: [{f1: 42}, {f1: null}]}, expectedQuery: {f1: {$in: [42, null]}}}, + { + actualQuery: {$or: [{f1: "a"}, {f1: "b"}, {f1: /c/}]}, + expectedQuery: {f1: {$in: ["a", "b", /c/]}}, + cannotRewriteWithCollation: true + }, ]; // These $or queries should not be rewritten into $in because of different semantics. @@ -133,22 +179,27 @@ for (const query of negativeTestQueries) { assertOrNotRewrittenToIn(query); } -function testOrToIn(queries) { +function testOrToIn(queries, usesCollation) { for (const queryPair of queries) { - assertEquivPlanAndResult(queryPair[0], queryPair[1]); + if (usesCollation && queryPair.cannotRewriteWithCollation) { + continue; + } + assertEquivPlanAndResult( + queryPair.actualQuery, queryPair.expectedQuery, !queryPair.cannotRewriteWithCollation); } } -testOrToIn(positiveTestQueries); // test without indexes +testOrToIn(positiveTestQueries, false /* usesCollation */); // test without indexes assert.commandWorked(coll.createIndex({f1: 1})); -testOrToIn(positiveTestQueries); // single index +testOrToIn(positiveTestQueries, false /* usesCollation */); // single index assert.commandWorked(coll.createIndex({f2: 1})); assert.commandWorked(coll.createIndex({f1: 1, f2: 1})); -testOrToIn(positiveTestQueries); // three indexes, requires multiplanning +testOrToIn(positiveTestQueries, + false /* usesCollation */); // three indexes, requires multiplanning // Test with a collection that has a collation, and that collation is the same as the query // collation @@ -156,12 +207,12 @@ coll.drop(); assert.commandWorked(db.createCollection("orToIn", {collation: {locale: 'en_US'}})); coll = db.orToIn; assert.commandWorked(coll.insert(data)); -testOrToIn(positiveTestQueries); +testOrToIn(positiveTestQueries, true /* usesCollation */); // Test with a collection that has a collation, and that collation is different from the query // collation coll.drop(); assert.commandWorked(db.createCollection("orToIn", {collation: {locale: 'de'}})); coll = db.orToIn; assert.commandWorked(coll.insert(data)); -testOrToIn(positiveTestQueries); +testOrToIn(positiveTestQueries, true /* usesCollation */); }()); diff --git a/jstests/core/project_with_collation.js b/jstests/core/project_with_collation.js new file mode 100644 index 00000000000..3f16716532f --- /dev/null +++ b/jstests/core/project_with_collation.js @@ -0,0 +1,183 @@ +// Tests to verify the behavior of find command's project in the presence of collation. +// +// @tags: [ +// assumes_no_implicit_collection_creation_after_drop, +// ] + +(function() { +'use strict'; + +const collation = { + locale: "en_US", + strength: 2 +}; +const withCollationCollName = jsTestName() + "_collation"; +const noCollationCollName = jsTestName() + "_noCollation"; + +function setupCollection(withCollation) { + const insertCollName = withCollation ? withCollationCollName : noCollationCollName; + db[insertCollName].drop(); + if (withCollation) { + assert.commandWorked(db.createCollection(insertCollName, {collation: withCollation})); + } + + const insertColl = db[insertCollName]; + assert.commandWorked(insertColl.insert( + {_id: 0, str: "a", array: [{str: "b"}, {str: "A"}, {str: "B"}, {str: "a"}]})); + assert.commandWorked(insertColl.insert({_id: 1, str: "a", elemMatch: [{str: "A"}, "ignored"]})); + assert.commandWorked(insertColl.insert({_id: 2, str: "A", elemMatch: ["ignored", {str: "a"}]})); + assert.commandWorked(insertColl.insert({_id: 3, str: "B"})); + + return insertColl; +} + +function runQueryWithCollation(testColl, collationToUse) { + let findCmd = + testColl.find({str: 'A', elemMatch: {$elemMatch: {str: "a"}}}, {_id: 1, 'elemMatch.$': 1}); + if (collationToUse) { + findCmd = findCmd.collation(collationToUse); + } + const elemMatchOutput = findCmd.toArray(); + assert.sameMembers(elemMatchOutput, + [{_id: 1, elemMatch: [{str: "A"}]}, {_id: 2, elemMatch: [{str: "a"}]}]); + + findCmd = + testColl.find({str: 'A'}, {sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}); + if (collationToUse) { + findCmd = findCmd.collation(collationToUse); + } + const sortArrayOutput = findCmd.toArray(); + assert.sameMembers(sortArrayOutput, [ + {_id: 0, sortedArray: [{str: "A"}, {str: "a"}, {str: "b"}, {str: "B"}]}, + {_id: 1, sortedArray: null}, + {_id: 2, sortedArray: null} + ]); + + const findAndUpdateOutput = testColl.findAndModify({ + query: {_id: 1, str: 'A', elemMatch: {$elemMatch: {str: "a"}}}, + fields: {_id: 1, 'elemMatch.$': 1, updated: 1}, + update: {$set: {updated: true}}, + collation: collationToUse + }); + assert.docEq(findAndUpdateOutput, {_id: 1, elemMatch: [{str: "A"}]}); + + const findAndUpdateWithSortArrayOutput = testColl.findAndModify({ + query: {_id: 0, str: 'A'}, + fields: {_id: 1, str: 1, sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}, + update: {$set: {updated: true}}, + collation: collationToUse, + new: true + }); + assert.docEq(findAndUpdateWithSortArrayOutput, + {_id: 0, str: "a", sortedArray: [{str: "A"}, {str: "a"}, {str: "b"}, {str: "B"}]}); + + const findAndRemoveOutput = testColl.findAndModify({ + query: {_id: 1, str: 'A', elemMatch: {$elemMatch: {str: "a"}}}, + fields: {_id: 1, 'elemMatch.$': 1, updated: 1}, + remove: true, + collation: collationToUse, + }); + assert.docEq(findAndRemoveOutput, {_id: 1, elemMatch: [{str: "A"}], updated: true}); + + const findAndRemoveWithSortArrayOutput = testColl.findAndModify({ + query: {_id: 0, str: 'A'}, + fields: {_id: 1, str: 1, sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}, + remove: true, + collation: collationToUse + }); + assert.docEq(findAndRemoveWithSortArrayOutput, + {_id: 0, str: "a", sortedArray: [{str: "A"}, {str: "a"}, {str: "b"}, {str: "B"}]}); +} + +// The output for the below two tests should not depend on the collection level collation. +let collWithCollation = setupCollection({locale: "en_US"}); +runQueryWithCollation(collWithCollation, collation); + +let noCollationColl = setupCollection(false); +runQueryWithCollation(noCollationColl, collation); + +// Tests to verify that the projection code inherits collection level collation in the absence of +// query level collation. +collWithCollation = setupCollection(collation); +runQueryWithCollation(collWithCollation, null); + +// The output of this should not depend on the collection level collation and simple collation +// should be applied always. +function queryWithSimpleCollation(testColl) { + const elemMatchOutput = + testColl.find({str: 'A', elemMatch: {$elemMatch: {str: "a"}}}, {_id: 1, 'elemMatch.$': 1}) + .collation({locale: "simple"}) + .toArray(); + assert.sameMembers(elemMatchOutput, [{_id: 2, elemMatch: [{str: "a"}]}]); + + const sortArrayOutput = + testColl.find({str: 'a'}, {sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}) + .collation({locale: "simple"}) + .toArray(); + assert.sameMembers(sortArrayOutput, [ + {_id: 0, sortedArray: [{str: "A"}, {str: "B"}, {str: "a"}, {str: "b"}]}, + {_id: 1, sortedArray: null} + ]); + + // Test findAndModify command with 'update'. Ensure that simple collation is always honored. + const findAndUpdateOutput = testColl.findAndModify({ + query: {str: 'A', elemMatch: {$elemMatch: {str: "a"}}}, + fields: {_id: 1, 'elemMatch.$': 1, updated: 1}, + update: {$set: {updated: true}}, + collation: {locale: "simple"} + }); + assert.docEq(findAndUpdateOutput, {_id: 2, elemMatch: [{str: "a"}]}); + + const findAndUpdateWithSortArrayOutput = testColl.findAndModify({ + query: {_id: 0, str: 'a'}, + fields: {_id: 1, str: 1, sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}, + update: {$set: {updated: true}}, + collation: {locale: "simple"}, + new: true + }); + assert.docEq(findAndUpdateWithSortArrayOutput, + {_id: 0, str: "a", sortedArray: [{str: "A"}, {str: "B"}, {str: "a"}, {str: "b"}]}); + + // Test findAndModify command with remove:true. Ensure that simple collation is always honored. + const findAndRemoveOutput = testColl.findAndModify({ + query: {_id: 2, str: 'A', elemMatch: {$elemMatch: {str: "a"}}}, + fields: {_id: 1, 'elemMatch.$': 1, updated: 1}, + remove: true, + collation: {locale: "simple"}, + }); + assert.docEq(findAndRemoveOutput, {_id: 2, elemMatch: [{str: "a"}], updated: true}); + + const findAndRemoveWithSortArrayOutput = testColl.findAndModify({ + query: {_id: 0, str: 'a'}, + fields: {_id: 1, str: 1, sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}, + remove: true, + collation: {locale: "simple"} + }); + assert.docEq(findAndRemoveWithSortArrayOutput, + {_id: 0, str: "a", sortedArray: [{str: "A"}, {str: "B"}, {str: "a"}, {str: "b"}]}); +} + +noCollationColl = setupCollection(false); +queryWithSimpleCollation(noCollationColl); + +collWithCollation = setupCollection(collation); +queryWithSimpleCollation(collWithCollation); + +// Test with views. +(function viewWithCollation() { + collWithCollation = setupCollection(collation); + db[jsTestName() + "_view"].drop(); + assert.commandWorked( + db.createView(jsTestName() + "_view", withCollationCollName, [], {collation: collation})); + const viewColl = db[jsTestName() + "_view"]; + + const sortArrayOutput = + viewColl.find({str: 'A'}, {sortedArray: {$sortArray: {input: "$array", sortBy: {str: 1}}}}) + .toArray(); + assert.sameMembers(sortArrayOutput, [ + {_id: 0, sortedArray: [{str: "A"}, {str: "a"}, {str: "b"}, {str: "B"}]}, + {_id: 1, sortedArray: null}, + {_id: 2, sortedArray: null} + ]); +})(); +})(); diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp index 94a301b9d3f..713f2eaa805 100644 --- a/src/mongo/db/commands/find_cmd.cpp +++ b/src/mongo/db/commands/find_cmd.cpp @@ -27,7 +27,6 @@ * it in the license file. */ - #include "mongo/platform/basic.h" #include "mongo/db/auth/authorization_checks.h" @@ -85,11 +84,17 @@ const auto kTermField = "term"_sd; boost::intrusive_ptr<ExpressionContext> makeExpressionContext( OperationContext* opCtx, const FindCommandRequest& findCommand, + const CollectionPtr& collPtr, boost::optional<ExplainOptions::Verbosity> verbosity) { std::unique_ptr<CollatorInterface> collator; if (!findCommand.getCollation().isEmpty()) { collator = uassertStatusOK(CollatorFactoryInterface::get(opCtx->getServiceContext()) ->makeFromBSON(findCommand.getCollation())); + } else if (collPtr && collPtr->getDefaultCollator()) { + // The 'collPtr' will be null for views, but we don't need to worry about views here. The + // views will get rewritten into aggregate command and will regenerate the + // ExpressionContext. + collator = collPtr->getDefaultCollator()->clone(); } // Although both 'find' and 'aggregate' commands have an ExpressionContext, some of the data @@ -288,7 +293,11 @@ public: // Finish the parsing step by using the FindCommandRequest to create a CanonicalQuery. const ExtensionsCallbackReal extensionsCallback(opCtx, &nss); - auto expCtx = makeExpressionContext(opCtx, *findCommand, verbosity); + + // The collection may be NULL. If so, getExecutor() should handle it by returning an + // execution tree with an EOFStage. + const auto& collection = ctx->getCollection(); + auto expCtx = makeExpressionContext(opCtx, *findCommand, collection, verbosity); const bool isExplain = true; auto cq = uassertStatusOK( CanonicalQuery::canonicalize(opCtx, @@ -340,10 +349,6 @@ public: return; } - // The collection may be NULL. If so, getExecutor() should handle it by returning an - // execution tree with an EOFStage. - const auto& collection = ctx->getCollection(); - cq->setUseCqfIfEligible(true); // Get the execution plan for the query. @@ -495,14 +500,14 @@ public: } // Tailing a replicated capped clustered collection requires majority read concern. - const auto coll = ctx->getCollection().get(); - if (coll) { + const auto& collection = ctx->getCollection(); + if (collection) { const bool isTailable = findCommand->getTailable(); const bool isMajorityReadConcern = repl::ReadConcernArgs::get(opCtx).getLevel() == repl::ReadConcernLevel::kMajorityReadConcern; - const bool isClusteredCollection = coll->isClustered(); - const bool isCapped = coll->isCapped(); - const bool isReplicated = coll->ns().isReplicated(); + const bool isClusteredCollection = collection->isClustered(); + const bool isCapped = collection->isCapped(); + const bool isReplicated = collection->ns().isReplicated(); if (isClusteredCollection && isCapped && isReplicated && isTailable) { uassert(ErrorCodes::Error(6049203), "A tailable cursor on a capped clustered collection requires majority " @@ -516,7 +521,9 @@ public: // Finish the parsing step by using the FindCommandRequest to create a CanonicalQuery. const ExtensionsCallbackReal extensionsCallback(opCtx, &nss); - auto expCtx = makeExpressionContext(opCtx, *findCommand, boost::none /* verbosity */); + + auto expCtx = + makeExpressionContext(opCtx, *findCommand, collection, boost::none /* verbosity */); auto cq = uassertStatusOK( CanonicalQuery::canonicalize(opCtx, std::move(findCommand), @@ -557,8 +564,6 @@ public: uassertStatusOK(replCoord->checkCanServeReadsFor( opCtx, nss, ReadPreferenceSetting::get(opCtx).canRunOnSecondary())); - const auto& collection = ctx->getCollection(); - if (cq->getFindCommandRequest().getReadOnce()) { // The readOnce option causes any storage-layer cursors created during plan // execution to assume read data will not be needed again and need not be cached. diff --git a/src/mongo/db/ops/parsed_delete.cpp b/src/mongo/db/ops/parsed_delete.cpp index 980d11d8f1b..ded493f1d63 100644 --- a/src/mongo/db/ops/parsed_delete.cpp +++ b/src/mongo/db/ops/parsed_delete.cpp @@ -147,4 +147,12 @@ std::unique_ptr<CanonicalQuery> ParsedDelete::releaseParsedQuery() { return std::move(_canonicalQuery); } +void ParsedDelete::setCollator(std::unique_ptr<CollatorInterface> collator) { + if (_canonicalQuery) { + _canonicalQuery->setCollator(std::move(collator)); + } else { + _expCtx->setCollator(std::move(collator)); + } +} + } // namespace mongo diff --git a/src/mongo/db/ops/parsed_delete.h b/src/mongo/db/ops/parsed_delete.h index ccf6b842884..6d6cb890681 100644 --- a/src/mongo/db/ops/parsed_delete.h +++ b/src/mongo/db/ops/parsed_delete.h @@ -115,6 +115,8 @@ public: return _expCtx; } + void setCollator(std::unique_ptr<CollatorInterface> collator); + private: // Transactional context. Not owned by us. OperationContext* _opCtx; diff --git a/src/mongo/db/query/get_executor.cpp b/src/mongo/db/query/get_executor.cpp index a37b18ddc8c..acbcab6d12d 100644 --- a/src/mongo/db/query/get_executor.cpp +++ b/src/mongo/db/query/get_executor.cpp @@ -1761,6 +1761,13 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> getExecutorDele expCtx->setIsCappedDelete(); } + // If the parsed delete does not have a user-specified collation, set it from the collection + // default. + if (collection && parsedDelete->getRequest()->getCollation().isEmpty() && + collection->getDefaultCollator()) { + parsedDelete->setCollator(collection->getDefaultCollator()->clone()); + } + if (collection && collection->isCapped() && opCtx->inMultiDocumentTransaction()) { // This check is duplicated from collection_internal::deleteDocument() for two reasons: // - Performing a remove on an empty capped collection would not call |