summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArun Banala <arun.banala@mongodb.com>2022-12-28 13:32:46 -0800
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2023-01-05 00:01:29 +0000
commit2f1b039897aa55eb3007abbe1020e6e4967b4140 (patch)
treed1927877b103a38a77b43ad172137eaa23e1d102
parent67c5ecd1847fe92b871ef9e26d3fb35f7bf1d662 (diff)
downloadmongo-2f1b039897aa55eb3007abbe1020e6e4967b4140.tar.gz
SERVER-72416 Find and findAndModify commands' ExpressionContext should inherit collection level collation
-rw-r--r--buildscripts/resmokeconfig/matrix_suites/overrides/replica_sets_stepdown_selector.yml2
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_multi_stmt_txn_kill_primary_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/replica_sets_reconfig_jscore_stepdown_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/retryable_writes_downgrade.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/retryable_writes_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/retryable_writes_jscore_stepdown_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/shard_split_kill_primary_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/shard_split_stepdown_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/shard_split_terminate_primary_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/sharded_retryable_writes_downgrade.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/tenant_migration_kill_primary_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/tenant_migration_stepdown_jscore_passthrough.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/tenant_migration_terminate_primary_jscore_passthrough.yml1
-rw-r--r--etc/backports_required_for_multiversion_tests.yml12
-rw-r--r--jstests/core/collation.js5
-rw-r--r--jstests/core/or_to_in.js133
-rw-r--r--jstests/core/project_with_collation.js183
-rw-r--r--src/mongo/db/commands/find_cmd.cpp33
-rw-r--r--src/mongo/db/ops/parsed_delete.cpp8
-rw-r--r--src/mongo/db/ops/parsed_delete.h2
-rw-r--r--src/mongo/db/query/get_executor.cpp7
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