summaryrefslogtreecommitdiff
path: root/jstests/core
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/core')
-rw-r--r--jstests/core/aggregation_accepts_write_concern.js6
-rw-r--r--jstests/core/api_version_pipeline_stages.js1
-rw-r--r--jstests/core/api_version_test_expression.js1
-rw-r--r--jstests/core/arrayfind8.js1
-rw-r--r--jstests/core/bypass_doc_validation.js1
-rw-r--r--jstests/core/bypass_empty_ts_replacement.js202
-rw-r--r--jstests/core/bypass_empty_ts_replacement_timeseries.js73
-rw-r--r--jstests/core/check_shard_index.js58
-rw-r--r--jstests/core/collmod.js14
-rw-r--r--jstests/core/command_let_variables_expressions.js208
-rw-r--r--jstests/core/currentop_shell.js177
-rw-r--r--jstests/core/elemmatch_index.js6
-rw-r--r--jstests/core/empty_ts.js134
-rw-r--r--jstests/core/explain_agg_write_concern.js1
-rw-r--r--jstests/core/explain_skip.js28
-rw-r--r--jstests/core/fts_mix.js1
-rw-r--r--jstests/core/index_filter_commands.js3
-rw-r--r--jstests/core/index_stats.js1
-rw-r--r--jstests/core/json_schema/json_schema.js2
-rw-r--r--jstests/core/json_schema/misc_validation.js1
-rw-r--r--jstests/core/negated_geo_queries.js93
-rw-r--r--jstests/core/notablescan.js86
-rw-r--r--jstests/core/opcounters_write_cmd.js1
-rw-r--r--jstests/core/plan_cache_list_plans.js1
-rw-r--r--jstests/core/profile_agg.js1
-rw-r--r--jstests/core/query/find_all_values_at_path_expression.js33
-rw-r--r--jstests/core/query/internal_strip_invalid_assignment.js5
-rw-r--r--jstests/core/rename_collection_system_db.js7
-rw-r--r--jstests/core/timeseries/libs/timeseries.js14
-rw-r--r--jstests/core/timeseries/timeseries_collmod.js13
-rw-r--r--jstests/core/timeseries/timeseries_computed_field.js856
-rw-r--r--jstests/core/timeseries/timeseries_filter_extended_range.js427
-rw-r--r--jstests/core/timeseries/timeseries_geonear_lookup.js40
-rw-r--r--jstests/core/timeseries/timeseries_graph_lookup.js1
-rw-r--r--jstests/core/timeseries/timeseries_insert_mixed_schema_bucket.js75
-rw-r--r--jstests/core/timeseries/timeseries_lookup.js90
-rw-r--r--jstests/core/timeseries/timeseries_merge.js1
-rw-r--r--jstests/core/timeseries/timeseries_project.js167
-rw-r--r--jstests/core/timeseries/timeseries_union_with.js1
-rw-r--r--jstests/core/timeseries/timeseries_update_mixed_schema_bucket.js79
-rw-r--r--jstests/core/txns/abort_expired_transaction.js13
-rw-r--r--jstests/core/txns/aggregation_in_transaction.js2
-rw-r--r--jstests/core/txns/prepare_conflict_aggregation_behavior.js7
-rw-r--r--jstests/core/views/invalid_system_views.js1
-rw-r--r--jstests/core/views/views_aggregation.js1
-rw-r--r--jstests/core/views/views_collation.js1
-rw-r--r--jstests/core/views/views_creation.js1
-rw-r--r--jstests/core/views/views_validation.js1
-rw-r--r--jstests/core/wildcard_index_validindex.js17
49 files changed, 2603 insertions, 351 deletions
diff --git a/jstests/core/aggregation_accepts_write_concern.js b/jstests/core/aggregation_accepts_write_concern.js
index 2c764414a1d..8117c296e03 100644
--- a/jstests/core/aggregation_accepts_write_concern.js
+++ b/jstests/core/aggregation_accepts_write_concern.js
@@ -1,7 +1,11 @@
/**
* Confirms that the aggregate command accepts writeConcern regardless of whether the pipeline
* writes or is read-only.
- * @tags: [assumes_write_concern_unchanged, does_not_support_stepdowns]
+ * @tags: [
+ * assumes_write_concern_unchanged,
+ * does_not_support_stepdowns,
+ * references_foreign_collection
+ * ]
*/
(function() {
"use strict";
diff --git a/jstests/core/api_version_pipeline_stages.js b/jstests/core/api_version_pipeline_stages.js
index c9772e4fa0c..c9d092a9a06 100644
--- a/jstests/core/api_version_pipeline_stages.js
+++ b/jstests/core/api_version_pipeline_stages.js
@@ -27,7 +27,6 @@ const unstablePipelines = [
[{$planCacheStats: {}}],
[{$unionWith: {coll: "coll2", pipeline: [{$collStats: {latencyStats: {}}}]}}],
[{$lookup: {from: "coll2", pipeline: [{$indexStats: {}}]}}],
- [{$lookup: {from: "coll2", _internalCollation: {locale: "simple"}}}],
[{$facet: {field1: [], field2: [{$indexStats: {}}]}}],
];
diff --git a/jstests/core/api_version_test_expression.js b/jstests/core/api_version_test_expression.js
index 41bbd9c0402..3252ebda83e 100644
--- a/jstests/core/api_version_test_expression.js
+++ b/jstests/core/api_version_test_expression.js
@@ -8,6 +8,7 @@
* assumes_unsharded_collection,
* uses_api_parameters,
* no_selinux,
+ * references_foreign_collection,
* ]
*/
diff --git a/jstests/core/arrayfind8.js b/jstests/core/arrayfind8.js
index 87a3a8d701a..ee74aae33ef 100644
--- a/jstests/core/arrayfind8.js
+++ b/jstests/core/arrayfind8.js
@@ -81,7 +81,6 @@ function checkQuery(subQuery, bothMatch, elemMatch, nonElemMatch, additionalCons
insertValueIfNotNull(bothMatch);
insertValueIfNotNull(elemMatch);
insertValueIfNotNull(nonElemMatch);
-
checkMatch(bothMatch, elemMatch, nonElemMatch, standardQuery, elemMatchQuery, 'unindexed');
// Check matching and index bounds for a single key index.
diff --git a/jstests/core/bypass_doc_validation.js b/jstests/core/bypass_doc_validation.js
index d290508835a..1fd32db658b 100644
--- a/jstests/core/bypass_doc_validation.js
+++ b/jstests/core/bypass_doc_validation.js
@@ -6,6 +6,7 @@
// uses_map_reduce_with_temp_collections,
// # Tenant migrations don't support applyOps.
// tenant_migration_incompatible,
+// references_foreign_collection,
// ]
/**
diff --git a/jstests/core/bypass_empty_ts_replacement.js b/jstests/core/bypass_empty_ts_replacement.js
new file mode 100644
index 00000000000..9bd74f3f0ca
--- /dev/null
+++ b/jstests/core/bypass_empty_ts_replacement.js
@@ -0,0 +1,202 @@
+/**
+ * Tests the "bypassEmptyTsReplacement" option.
+ *
+ * @tags: [
+ * requires_fcv_60,
+ * ]
+ */
+(function() {
+"use strict";
+
+const coll = db.jstests_bypass_empty_ts_replacement;
+const collName = coll.getName();
+const emptyTs = Timestamp(0, 0);
+
+coll.drop();
+
+function doInsert(docs, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand(
+ {insert: collName, documents: docs, bypassEmptyTsReplacement: bypassEmptyTsReplacement});
+ assert.commandWorked(cmdRes);
+}
+
+function doUpdate(filter, update, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand({
+ update: collName,
+ updates: [{q: filter, u: update}],
+ bypassEmptyTsReplacement: bypassEmptyTsReplacement
+ });
+ assert.commandWorked(cmdRes);
+}
+
+function doUpdateWithUpsert(filter, update, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand({
+ update: collName,
+ updates: [{q: filter, u: update, upsert: true}],
+ bypassEmptyTsReplacement: bypassEmptyTsReplacement
+ });
+ assert.commandWorked(cmdRes);
+}
+
+function doFindAndModify(filter, update, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand({
+ findAndModify: collName,
+ query: filter,
+ update: update,
+ bypassEmptyTsReplacement: bypassEmptyTsReplacement
+ });
+ assert.commandWorked(cmdRes);
+}
+
+function doFindAndModifyWithUpsert(filter, update, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand({
+ findAndModify: collName,
+ query: filter,
+ update: update,
+ upsert: true,
+ bypassEmptyTsReplacement: bypassEmptyTsReplacement
+ });
+ assert.commandWorked(cmdRes);
+}
+
+let numCallsToRunTests = 0;
+
+function runTests(bypassEmptyTsReplacement) {
+ ++numCallsToRunTests;
+
+ let first = (numCallsToRunTests == 1);
+ let startId = numCallsToRunTests * 100;
+
+ function getId(i) {
+ return startId + i;
+ }
+
+ // The insert/update/findAndModify commands below will be run with the bypassEmptyTsReplacement
+ // parameter set to true or false (depending whether the 'bypassEmptyTsReplacement' variable is
+ // true or false).
+
+ // Insert several documents.
+ doInsert([{_id: getId(1), a: emptyTs}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(2), a: 1}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(3), a: 2}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(6), a: 3}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(7), a: 4}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(8), a: 5}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(9), a: 6}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(10), a: 7}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(11), a: 8}], bypassEmptyTsReplacement);
+
+ // Use a replacement-style update to update the doc with _id=getId(2).
+ //
+ // When bypassEmptyTsReplacement=true, this is an example of how the special "empty timestamp"
+ // behavior can be suppressed when doing a replacement-style update.
+ doUpdate({_id: getId(2)}, {a: emptyTs}, bypassEmptyTsReplacement);
+
+ // Use a replacement-style findAndModify to update the doc with _id=getId(3).
+ doFindAndModify({_id: getId(3)}, {a: emptyTs}, bypassEmptyTsReplacement);
+
+ // Do a replacement-style update to add a new document with _id=getId(4).
+ doUpdateWithUpsert({_id: getId(4)}, {a: emptyTs}, bypassEmptyTsReplacement);
+
+ // Do a replacement-style findAndModify to add a new document with _id=getId(5).
+ doFindAndModifyWithUpsert({_id: getId(5)}, {a: emptyTs}, bypassEmptyTsReplacement);
+
+ // Do an update-operator-style update to update the doc with _id=getId(6).
+ doUpdate({_id: getId(6)}, {$set: {a: emptyTs}}, bypassEmptyTsReplacement);
+
+ // Do an update-operator-style findAndModify to update the doc with _id=getId(7).
+ doFindAndModify({_id: getId(7)}, {$set: {a: emptyTs}}, bypassEmptyTsReplacement);
+
+ // Do a pipeline-style update to update the doc with _id=getId(8).
+ doUpdate({_id: getId(8)}, [{$addFields: {a: emptyTs}}], bypassEmptyTsReplacement);
+
+ // Do a pipeline-style findAndModify to update the doc with _id=getId(9).
+ doFindAndModify({_id: getId(9)}, [{$addFields: {a: emptyTs}}], bypassEmptyTsReplacement);
+
+ // Do a pipeline-style update with $internalApplyOplogUpdate to update the doc with
+ // _id=getId(10).
+ doUpdate({_id: getId(10)},
+ [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}],
+ bypassEmptyTsReplacement);
+
+ // Do a pipeline-style findAndModify with $internalApplyOplogUpdate to update the doc with
+ // _id=getId(11).
+ doFindAndModify({_id: getId(11)},
+ [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}],
+ bypassEmptyTsReplacement);
+
+ // Do an update-operator-style update to add a new document with _id=getId(12).
+ doUpdateWithUpsert({_id: getId(12)}, {$set: {a: emptyTs}}, bypassEmptyTsReplacement);
+
+ // Do an update-operator-style findAndModify to add a new document with _id=getId(13).
+ doFindAndModifyWithUpsert({_id: getId(13)}, {$set: {a: emptyTs}}, bypassEmptyTsReplacement);
+
+ // Do a pipeline-style update to add a new document with _id=getId(14).
+ doUpdateWithUpsert({_id: getId(14)}, [{$addFields: {a: emptyTs}}], bypassEmptyTsReplacement);
+
+ // Do a pipeline-style findAndModify to add a new document with _id=getId(15).
+ doFindAndModifyWithUpsert(
+ {_id: getId(15)}, [{$addFields: {a: emptyTs}}], bypassEmptyTsReplacement);
+
+ // Do a pipline-style update with $internalApplyOplogUpdate to add a new document with
+ // _id=getId(16).
+ doUpdateWithUpsert(
+ {_id: getId(16)},
+ [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}],
+ bypassEmptyTsReplacement);
+
+ // Do pipeline-style findAndModify with $internalApplyOplogUpdate to add a new document
+ // with _id=getId(17).
+ doFindAndModifyWithUpsert(
+ {_id: getId(17)},
+ [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}],
+ bypassEmptyTsReplacement);
+
+ // Verify that all the insert, update, and findAndModify commands behaved the way we expected.
+ // If 'bypassEmptyTsReplacement' is true, then we expect field "a" will be equal to the empty
+ // timestamp for all 17 documents. If 'bypassEmptyTsReplacement' is false, then we expect
+ // field "a" will be equal to the current timestamp for the first 5 documents and it will
+ // be equal to the empty timestamp for the other 12 documents.
+ for (let i = 1; i <= 17; ++i) {
+ let result = coll.findOne({_id: getId(i)});
+
+ if (bypassEmptyTsReplacement || i >= 6) {
+ assert.eq(tojson(result.a), tojson(emptyTs), "_id=" + getId(i));
+ } else {
+ assert.neq(tojson(result.a), tojson(emptyTs), "_id=" + getId(i));
+ }
+ }
+
+ if (first) {
+ // If this is the first time that runTests() is invoked, insert a document with
+ // _id=Timestamp(0,0), and then verify that the document we just inserted can be
+ // retrieved using the filter "{_id: Timestamp(0,0)}".
+ doInsert([{_id: emptyTs, a: 9}], bypassEmptyTsReplacement);
+
+ let result = coll.findOne({_id: emptyTs});
+ assert.eq(tojson(result._id), tojson(emptyTs), "_id=" + tojson(emptyTs));
+ assert.eq(tojson(result.a), tojson(9), "_id=" + tojson(emptyTs));
+ }
+
+ // Do a replacement-style update on the document with _id=Timestamp(0,0).
+ doUpdate({_id: emptyTs}, {_id: emptyTs, a: emptyTs}, bypassEmptyTsReplacement);
+
+ // Verify the document we just updated can still be retrieved using "{_id: Timestamp(0,0)}".
+ let result = coll.findOne({_id: emptyTs});
+ assert.eq(tojson(result._id), tojson(emptyTs), "_id=" + tojson(emptyTs));
+
+ if (bypassEmptyTsReplacement) {
+ // Verify that an empty timestamp value was stored in field "a".
+ assert.eq(tojson(result.a), tojson(emptyTs), "_id=" + tojson(emptyTs));
+ } else {
+ // Verify that field "a" was set to the current timestamp.
+ assert.neq(tojson(result.a), tojson(emptyTs), "_id=" + tojson(emptyTs));
+ }
+}
+
+let bypassEmptyTsReplacement = true;
+runTests(bypassEmptyTsReplacement);
+
+bypassEmptyTsReplacement = false;
+runTests(bypassEmptyTsReplacement);
+}());
diff --git a/jstests/core/bypass_empty_ts_replacement_timeseries.js b/jstests/core/bypass_empty_ts_replacement_timeseries.js
new file mode 100644
index 00000000000..69a5bcf281c
--- /dev/null
+++ b/jstests/core/bypass_empty_ts_replacement_timeseries.js
@@ -0,0 +1,73 @@
+/**
+ * Tests inserts, updates, and upserts on a timeseries collection with "bypassEmptyTsReplacement"
+ * set to true.
+ *
+ * @tags: [
+ * assumes_no_implicit_collection_creation_after_drop,
+ * does_not_support_transactions,
+ * requires_fcv_60,
+ * requires_multi_updates,
+ * requires_non_retryable_writes,
+ * requires_timeseries,
+ * ]
+ */
+(function() {
+"use strict";
+
+const coll = db.jstests_bypass_empty_ts_replacement_timeseries;
+const collName = coll.getName();
+const emptyTs = Timestamp(0, 0);
+
+coll.drop();
+
+assert.commandWorked(db.createCollection(collName, {timeseries: {timeField: "t", metaField: "m"}}));
+
+function doInsert(docs, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand(
+ {insert: collName, documents: docs, bypassEmptyTsReplacement: bypassEmptyTsReplacement});
+ assert.commandWorked(cmdRes);
+}
+
+function doUpdate(filter, update, bypassEmptyTsReplacement) {
+ let cmdRes = db.runCommand({
+ update: collName,
+ updates: [{q: filter, u: update, multi: true}],
+ bypassEmptyTsReplacement: bypassEmptyTsReplacement
+ });
+ assert.commandWorked(cmdRes);
+}
+
+let numCallsToRunTests = 0;
+
+function runTests(bypassEmptyTsReplacement) {
+ ++numCallsToRunTests;
+
+ let startId = numCallsToRunTests * 100;
+
+ function getId(i) {
+ return startId + i;
+ }
+
+ // Insert two documents.
+ doInsert([{_id: getId(1), t: new Date(100), m: emptyTs, a: emptyTs}], bypassEmptyTsReplacement);
+ doInsert([{_id: getId(2), t: new Date(200), m: getId(2), a: emptyTs}],
+ bypassEmptyTsReplacement);
+
+ // Do a replacement-style update containing an empty timestamp value.
+ doUpdate({m: getId(2)}, {$set: {m: emptyTs}}, bypassEmptyTsReplacement);
+
+ // Verify that the commands above didn't mutate any of the empty timestamp values in the
+ // collection.
+ for (let i = 1; i <= 2; ++i) {
+ let result = coll.findOne({_id: getId(i)});
+ assert.eq(tojson(result.m), tojson(emptyTs), "_id=" + getId(i));
+ assert.eq(tojson(result.a), tojson(emptyTs), "_id=" + getId(i));
+ }
+}
+
+let bypassEmptyTsReplacement = true;
+runTests(bypassEmptyTsReplacement);
+
+bypassEmptyTsReplacement = false;
+runTests(bypassEmptyTsReplacement);
+}());
diff --git a/jstests/core/check_shard_index.js b/jstests/core/check_shard_index.js
index 2beb3c12891..23f69c6b757 100644
--- a/jstests/core/check_shard_index.js
+++ b/jstests/core/check_shard_index.js
@@ -21,12 +21,12 @@ f.createIndex({x: 1, y: 1});
assert.eq(0, f.count(), "1. initial count should be zero");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(true, res.ok, "1a");
+assert.commandWorked(res, "1a " + tojson(res));
f.save({x: 1, y: 1});
assert.eq(1, f.count(), "1. count after initial insert should be 1");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(true, res.ok, "1b");
+assert.commandWorked(res, "1b " + tojson(res));
// -------------------------
// Case 2: entry with null values would make an index suitable
@@ -40,16 +40,16 @@ f.save({x: 1, y: 1});
f.save({x: null, y: 1});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(true, res.ok, "2a " + tojson(res));
+assert.commandWorked(res, "2a " + tojson(res));
f.save({y: 2});
assert.eq(3, f.count(), "2. count after initial insert should be 3");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(true, res.ok, "2b " + tojson(res));
+assert.commandWorked(res, "2b " + tojson(res));
// Check _id index
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {_id: 1}});
-assert.eq(true, res.ok, "2c " + tojson(res));
+assert.commandWorked(res, "2c " + tojson(res));
assert(res.idskip, "2d " + tojson(res));
// -------------------------
@@ -65,7 +65,7 @@ f.save({x: [1, 2], y: 2});
assert.eq(2, f.count(), "3. count after initial insert should be 2");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok, "3a " + tojson(res));
+assert.commandFailed(res, "3a " + tojson(res));
f.remove({y: 2});
f.dropIndex({x: 1, y: 1});
@@ -73,13 +73,13 @@ f.createIndex({x: 1, y: 1});
assert.eq(1, f.count(), "3. count after removing array value should be 1");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(true, res.ok, "3b " + tojson(res));
+assert.commandWorked(res, "3b " + tojson(res));
f.save({x: 2, y: [1, 2]});
assert.eq(2, f.count(), "3. count after adding array value should be 2");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok, "3c " + tojson(res));
+assert.commandFailed(res, "3c " + tojson(res));
// -------------------------
// Case 4: Handles prefix shard key indexes.
@@ -93,21 +93,21 @@ f.save({x: 1, y: 1, z: 1});
assert.eq(1, f.count(), "4. count after initial insert should be 1");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1}});
-assert.eq(true, res.ok, "4a " + tojson(res));
+assert.commandWorked(res, "4a " + tojson(res));
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(true, res.ok, "4b " + tojson(res));
+assert.commandWorked(res, "4b " + tojson(res));
f.save({x: [1, 2], y: 2, z: 2});
assert.eq(2, f.count(), "4. count after adding array value should be 2");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1}});
-assert.eq(false, res.ok, "4c " + tojson(res));
+assert.commandFailed(res, "4c " + tojson(res));
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok, "4d " + tojson(res));
+assert.commandFailed(res, "4d " + tojson(res));
res = db.runCommand(
{checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1, z: 1}});
-assert.eq(false, res.ok, "4e " + tojson(res));
+assert.commandFailed(res, "4e " + tojson(res));
f.remove({y: 2});
f.dropIndex({x: 1, y: 1, z: 1});
@@ -116,18 +116,18 @@ f.createIndex({x: 1, y: 1, z: 1});
assert.eq(1, f.count(), "4. count after removing array value should be 1");
res = db.runCommand(
{checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1, z: 1}});
-assert.eq(true, res.ok, "4f " + tojson(res));
+assert.commandWorked(res, "4f " + tojson(res));
f.save({x: 3, y: [1, 2], z: 3});
assert.eq(2, f.count(), "4. count after adding array value on second key should be 2");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1}});
-assert.eq(false, res.ok, "4g " + tojson(res));
+assert.commandFailed(res, "4g " + tojson(res));
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok, "4h " + tojson(res));
+assert.commandFailed(res, "4h " + tojson(res));
res = db.runCommand(
{checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1, z: 1}});
-assert.eq(false, res.ok, "4i " + tojson(res));
+assert.commandFailed(res, "4i " + tojson(res));
f.remove({x: 3});
// Necessary so that the index is no longer marked as multikey
@@ -137,18 +137,18 @@ f.createIndex({x: 1, y: 1, z: 1});
assert.eq(1, f.count(), "4. count after removing array value should be 1 again");
res = db.runCommand(
{checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1, z: 1}});
-assert.eq(true, res.ok, "4e " + tojson(res));
+assert.commandWorked(res, "4e " + tojson(res));
f.save({x: 4, y: 4, z: [1, 2]});
assert.eq(2, f.count(), "4. count after adding array value on third key should be 2");
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1}});
-assert.eq(false, res.ok, "4c " + tojson(res));
+assert.commandFailed(res, "4c " + tojson(res));
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok, "4d " + tojson(res));
+assert.commandFailed(res, "4d " + tojson(res));
res = db.runCommand(
{checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1, z: 1}});
-assert.eq(false, res.ok, "4e " + tojson(res));
+assert.commandFailed(res, "4e " + tojson(res));
// -------------------------
// Test error messages of checkShardingIndex failing:
@@ -157,21 +157,21 @@ assert.eq(false, res.ok, "4e " + tojson(res));
f.drop();
f.createIndex({x: 1});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Shard key is not a prefix of index key."));
// Index key is partial:
f.drop();
f.createIndex({x: 1, y: 1}, {partialFilterExpression: {y: {$gt: 0}}});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index key is partial."));
// Index key is sparse:
f.drop();
f.createIndex({x: 1, y: 1}, {sparse: true});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index key is sparse."));
// Index key is multikey:
@@ -179,21 +179,21 @@ f.drop();
f.createIndex({x: 1, y: 1});
f.save({y: [1, 2, 3, 4, 5]});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index key is multikey."));
// Index key has a non-simple collation:
f.drop();
f.createIndex({x: 1, y: 1}, {collation: {locale: "en"}});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index has a non-simple collation."));
// Index key is sparse and index has non-simple collation:
f.drop();
f.createIndex({x: 1, y: 1}, {sparse: true, collation: {locale: "en"}});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index key is sparse.") &&
res.errmsg.includes("Index has a non-simple collation."));
@@ -203,7 +203,7 @@ f.createIndex({x: 1, y: 1}, {name: "index_1_part", partialFilterExpression: {x:
f.createIndex({x: 1, y: 1}, {name: "index_2"});
f.save({y: [1, 2, 3, 4, 5]});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index key is multikey.") &&
res.errmsg.includes("Index key is partial."));
@@ -212,7 +212,7 @@ f.drop();
f.createIndex({x: 1, y: 1}, {name: "index_1_part", partialFilterExpression: {x: {$gt: 0}}});
f.createIndex({x: 1, y: 1}, {name: "index_2_sparse", sparse: true});
res = db.runCommand({checkShardingIndex: "test.jstests_shardingindex", keyPattern: {x: 1, y: 1}});
-assert.eq(false, res.ok);
+assert.commandFailed(res);
assert(res.errmsg.includes("Index key is partial.") && res.errmsg.includes("Index key is sparse."));
print("PASSED");
diff --git a/jstests/core/collmod.js b/jstests/core/collmod.js
index 4362fc110a4..3362650a77a 100644
--- a/jstests/core/collmod.js
+++ b/jstests/core/collmod.js
@@ -131,3 +131,17 @@ assert.commandFailed(
// Fails with an unknown key pattern.
assert.commandFailed(db.runCommand(
{collmod: coll, index: {keyPattern: {doesnotexist: 1}, expireAfterSeconds: 100}}));
+
+// Exclude from multiversion suites as SERVER-84531 will not be backported to older versions
+const isMultiversion = jsTestOptions().mixedBinVersions || jsTestOptions().shardMixedBinVersions ||
+ jsTestOptions().useRandomBinVersionsWithinReplicaSet;
+if (!isMultiversion) {
+ // The timeseriesBucketsMayHaveMixedSchemaData option can only be used on time-series
+ // collections.
+ assert.commandFailedWithCode(
+ db.runCommand({collMod: coll, timeseriesBucketsMayHaveMixedSchemaData: true}),
+ ErrorCodes.InvalidOptions);
+ assert.commandFailedWithCode(
+ db.runCommand({collMod: coll, timeseriesBucketsMayHaveMixedSchemaData: false}),
+ ErrorCodes.InvalidOptions);
+}
diff --git a/jstests/core/command_let_variables_expressions.js b/jstests/core/command_let_variables_expressions.js
new file mode 100644
index 00000000000..c551c30be48
--- /dev/null
+++ b/jstests/core/command_let_variables_expressions.js
@@ -0,0 +1,208 @@
+// Tests that commands like find, aggregate and update accept a 'let' parameter which defines
+// variables for use in expressions within the command. Specifically covers cases where the
+// parameter definitions contain expressions.
+// @tags: [
+// requires_non_retryable_writes,
+// does_not_support_transactions,
+// tenant_migration_incompatible
+// ]
+(function() {
+"use strict";
+
+load('jstests/aggregation/extras/utils.js'); // For assertArrayEq().
+
+const coll = db.getCollection('command_let_variables_expressions');
+
+function setupColl() {
+ coll.drop();
+ assert.commandWorked(coll.insert([
+ {_id: 2, a: {}},
+ {_id: 3, b: 1},
+ {_id: 4, a: "$notAFieldPath"},
+ ]));
+}
+setupColl();
+
+const missingLetParam = {
+ $getField: {field: "c", input: {a: 1}}
+};
+const literalLetParam = {
+ $literal: "$notAFieldPath"
+};
+
+// Run find commands with 'let' parameters containing expressions.
+{
+ // 'let' parameter expression that resolves to a missing value should not cause a query error.
+ let result = assert.commandWorked(db.runCommand({
+ find: coll.getName(),
+ filter: {$expr: {$eq: ["$a", "$$c"]}},
+ let : {c: missingLetParam},
+ }));
+ assertArrayEq({
+ actual: result.cursor.firstBatch,
+ expected: [{_id: 3, b: 1}],
+ });
+
+ // 'let' parameter expression that includes $literal and a $-prefixed string should not be
+ // misinterpreted as a field path.
+ result = assert.commandWorked(db.runCommand({
+ find: coll.getName(),
+ filter: {$expr: {$eq: ["$a", "$$c"]}},
+ let : {c: literalLetParam},
+ }));
+ assertArrayEq({
+ actual: result.cursor.firstBatch,
+ expected: [{_id: 4, a: "$notAFieldPath"}],
+ });
+}
+
+// Run aggregate commands with 'let' parameters containing expressions.
+{
+ let result =
+ coll.aggregate([{$match: {$expr: {$eq: ["$a", "$$c"]}}}], {let : {c: missingLetParam}})
+ .toArray();
+ assertArrayEq({
+ actual: result,
+ expected: [{_id: 3, b: 1}],
+ });
+
+ result = coll.aggregate([{$match: {$expr: {$eq: ["$a", "$$c"]}}}], {let : {c: literalLetParam}})
+ .toArray();
+ assertArrayEq({
+ actual: result,
+ expected: [{_id: 4, a: "$notAFieldPath"}],
+ });
+}
+
+// Same thing but for update.
+{
+ assert.commandWorked(db.runCommand({
+ update: coll.getName(),
+ updates: [{
+ q: {$expr: {$eq: ["$a", "$$c"]}},
+ u: [{$set: {c: "updated"}}],
+ multi: true,
+ }],
+ let : {c: missingLetParam}
+ }));
+ assertArrayEq({
+ actual: coll.find().toArray(),
+ expected: [{_id: 2, a: {}}, {_id: 3, b: 1, c: "updated"}, {_id: 4, a: "$notAFieldPath"}],
+ });
+
+ // Undo changes.
+ setupColl();
+
+ assert.commandWorked(db.runCommand({
+ update: coll.getName(),
+ updates: [{
+ q: {$expr: {$eq: ["$a", "$$c"]}},
+ u: [{$set: {c: "updated"}}],
+ multi: true,
+ }],
+ let : {c: literalLetParam}
+ }));
+ assertArrayEq({
+ actual: coll.find().toArray(),
+ expected: [
+ {_id: 2, a: {}},
+ {_id: 3, b: 1},
+ {_id: 4, a: "$notAFieldPath", c: "updated"},
+ ],
+ });
+}
+
+// Run findAndModify commands with 'let' parameters containing expressions.
+{
+ setupColl();
+
+ assert.commandWorked(db.runCommand({
+ findAndModify: coll.getName(),
+ query: {_id: 3, $expr: {$eq: ["$a", "$$c"]}},
+ update: [{$set: {c: "updated"}}],
+ let : {c: missingLetParam}
+ }));
+ assertArrayEq({
+ actual: coll.find().toArray(),
+ expected: [{_id: 2, a: {}}, {_id: 3, b: 1, c: "updated"}, {_id: 4, a: "$notAFieldPath"}],
+ });
+
+ // Undo changes.
+ setupColl();
+
+ assert.commandWorked(db.runCommand({
+ findAndModify: coll.getName(),
+ query: {_id: 4, $expr: {$eq: ["$a", "$$c"]}},
+ update: [{$set: {c: "updated"}}],
+ let : {c: literalLetParam}
+ }));
+ assertArrayEq({
+ actual: coll.find().toArray(),
+ expected: [
+ {_id: 2, a: {}},
+ {_id: 3, b: 1},
+ {_id: 4, a: "$notAFieldPath", c: "updated"},
+ ],
+ });
+}
+
+// Run delete commands with 'let' parameters containing expressions.
+{
+ setupColl();
+
+ assert.commandWorked(db.runCommand({
+ delete: coll.getName(),
+ deletes: [{
+ q: {$expr: {$eq: ["$a", "$$c"]}},
+ limit: 0, // multi
+ }],
+ let : {c: missingLetParam}
+ }));
+ assertArrayEq({
+ actual: coll.find().toArray(),
+ expected: [{_id: 2, a: {}}, {_id: 4, a: "$notAFieldPath"}],
+ });
+
+ // Undo changes.
+ setupColl();
+
+ assert.commandWorked(db.runCommand({
+ delete: coll.getName(),
+ deletes: [{
+ q: {$expr: {$eq: ["$a", "$$c"]}},
+ limit: 0, // multi
+ }],
+ let : {c: literalLetParam}
+ }));
+ assertArrayEq({
+ actual: coll.find().toArray(),
+ expected: [{_id: 2, a: {}}, {_id: 3, b: 1}],
+ });
+}
+
+// Run a $lookup with let/pipeline syntax. In sharded environments, this will require serializing
+// 'let' variables to send to the shards. One of the documents in the collection is missing the
+// local field ("$a"), which will cause a missing 'let' variable to be serialized.
+{
+ setupColl();
+
+ const result = coll.aggregate([
+ {
+ $lookup: {
+ from: coll.getName(),
+ as: "res",
+ let: {local_a: "$a"},
+ pipeline: [{$match: {$expr: {$eq: ["$$local_a", "$a"]},}}, {$project: {_id: 1}}]
+ }
+ }
+ ]).toArray();
+ assertArrayEq({
+ actual: result,
+ expected: [
+ {_id: 2, a: {}, res: [{_id: 2}]},
+ {_id: 3, b: 1, res: [{_id: 3}]},
+ {_id: 4, a: "$notAFieldPath", res: [{_id: 4}]},
+ ],
+ });
+}
+}());
diff --git a/jstests/core/currentop_shell.js b/jstests/core/currentop_shell.js
index c96ef2507f1..31b084bf132 100644
--- a/jstests/core/currentop_shell.js
+++ b/jstests/core/currentop_shell.js
@@ -2,7 +2,16 @@
* Tests that the shell helper db.currentOpCursor isn't constrained by the legacy currentOp server
* command - ie. the result set isn't limited to 16MB and long operations aren't truncated.
*
+ * Note: On newer branches, this test contains additional cases for the currentOp command (without a
+ * shell helper) and the $currentOp pipeline stage, which are not included here. Those cases would
+ * behave unreliably because of SERVER-92284.
+ *
* @tags: [
+ * # The collection may be completely moved to another shard, which results in currentOp not
+ * # returning the expected command.
+ * assumes_balancer_off,
+ * # The test runs commands that are not allowed with security token: getLog.
+ * not_allowed_with_signed_security_token,
* uses_parallel_shell,
* # This test uses currentOp to check whether an aggregate command is running. In replica set
* # environments, because currentOp is run against the admin database it is routed to the
@@ -11,6 +20,8 @@
* # currentOp results.
* assumes_read_preference_unchanged,
* no_selinux,
+ * # Uses $function operator.
+ * requires_scripting,
* ]
*/
@@ -18,31 +29,45 @@
"use strict";
load("jstests/libs/fixture_helpers.js"); // for FixtureHelpers
+load('jstests/libs/parallel_shell_helpers.js');
const coll = db.currentOp_cursor;
coll.drop();
-for (let i = 0; i < 3; i++) {
+for (let i = 0; i < 100; i++) {
assert.commandWorked(coll.insert({val: 1}));
}
+//
// Test that db.currentOpCursor() returns an iterable cursor.
-let res = db.currentOpCursor();
-assert(res.hasNext());
-assert(res.next());
-
-// Test that db.currentOp() interface does not change.
-res = db.currentOp();
-assert("inprog" in res, "Result contains 'inprog' field");
-assert("ok" in res, "Result contains 'ok' field");
-
-// Attempting to access the fsyncLock field from the results throws with an error message.
-let error = assert.throws(() => res.fsyncLock);
+//
+const cursorFromCurrentOp = db.currentOpCursor();
+assert(cursorFromCurrentOp.hasNext());
+assert(cursorFromCurrentOp.next());
+
+//
+// Test that db.currentOp() returns an object in the expected format.
+//
+const currentOpRes = db.currentOp();
+assert("inprog" in currentOpRes, "Result contains 'inprog' field");
+assert("ok" in currentOpRes, "Result contains 'ok' field");
+
+//
+// Test that attempting to access the fsyncLock field from the results throws with an error message.
+//
+const error = assert.throws(() => currentOpRes.fsyncLock);
assert(
/fsyncLock is no longer included in the currentOp shell helper, run db\.runCommand\({currentOp: 1}\) instead/
.test(error));
-function shellOp() {
+//
+// Start a pipeline with a large command object in a parallel shell and then test three different
+// methods of executing "currentOp" queries to ensure that they all observe the operation and that
+// they do or do not truncate its command object (according to each one's specification).
+//
+
+// Starts the query. Intended to b e called from a parallel shell.
+function startLongRunningAggregation(collName, comment) {
function createLargeDoc() {
let doc = {};
for (let i = 0; i < 100; i++) {
@@ -52,90 +77,96 @@ function shellOp() {
}
assert.commandFailedWithCode(db.runCommand({
- aggregate: "currentOp_cursor",
+ aggregate: collName,
pipeline: [{
$addFields: {
newVal: {$function: {args: [], body: "sleep(1000000)", lang: "js"}},
bigDoc: createLargeDoc()
}
}],
- comment: TestData.comment,
+ comment: comment,
cursor: {}
}),
ErrorCodes.Interrupted);
}
-function startShellWithOp(comment) {
- TestData.comment = comment;
- const awaitShell = startParallelShell(shellOp);
-
- // Confirm that the operation has started in the parallel shell.
+// Repeatedly executes 'getOperationsFunction()' until it returns exactly one operation for each
+// shard in a sharded collection or exactly one operation for an unsharded collection.
+function awaitOperations(getOperationsFunction) {
+ let operations;
assert.soon(
function() {
- let aggRes =
- db.getSiblingDB("admin")
- .aggregate([
- {$currentOp: {}},
- {$match: {ns: "test.currentOp_cursor", "command.comment": TestData.comment}}
- ])
- .toArray();
- return aggRes.length >= 1;
+ const numShards = FixtureHelpers.numberOfShardsForCollection(coll);
+ operations = getOperationsFunction();
+
+ // No shard should have more than one operation matching the query comment. First check
+ // that the total number of operations is no greater than the total number of shards.
+ assert.lte(operations.length, numShards, operations);
+
+ // Also explicitly check that each shard appears no more than once in the list of
+ // operations.
+ const distinctShardNames = new Set(operations.map(op => "shard" in op ? op.shard : ""));
+ assert.eq(operations.length, distinctShardNames.size, {operations, numShards});
+
+ if (operations.length < numShards) {
+ print(`Found ${operations.length} operation(s); waiting until there are ${
+ numShards} operation(s)`);
+ return false;
+ } else if (operations.some(op => op.op !== "getmore" && "cursor" in op &&
+ op.cursor.batchSize === 0)) {
+ print(`Found command with empty 'batchSize' value; waiting for getmore: ${
+ tojson(operations)}`);
+ return false;
+ }
+
+ return true;
},
function() {
return "Failed to find parallel shell operation in $currentOp output: " +
tojson(db.currentOp());
});
- return awaitShell;
-}
-// Test that the currentOp server command truncates long operations with a warning logged.
-const serverCommandTest = startShellWithOp("currentOp_server");
-res = db.adminCommand({
- currentOp: true,
- $and: [{"ns": "test.currentOp_cursor"}, {"command.comment": "currentOp_server"}]
-});
+ return operations;
+}
-if (FixtureHelpers.isMongos(db) && FixtureHelpers.isSharded(coll)) {
- // Assert currentOp truncation behavior for each shard in the cluster.
- assert(res.inprog.length >= 1, res);
- res.inprog.forEach((result) => {
- assert.eq(result.op, "getmore", result);
- assert(result.cursor.originatingCommand.hasOwnProperty("$truncated"), result);
- });
-} else {
- // Assert currentOp truncation behavior for unsharded collections.
- assert.eq(res.inprog.length, 1, res);
- assert.eq(res.inprog[0].op, "command", res);
- assert(res.inprog[0].command.hasOwnProperty("$truncated"), res);
+function getCommandFromCurrentOpEntry(entry) {
+ if (entry.op === "command" && "command" in entry) {
+ return entry.command;
+ } else if (entry.op === "getmore" && "cursor" in entry &&
+ "originatingCommand" in entry.cursor) {
+ return entry.cursor.originatingCommand;
+ } else {
+ assert(false, entry);
+ }
}
-const log = FixtureHelpers.getPrimaryForNodeHostingDatabase(db).adminCommand({getLog: "global"});
-assert(/will be truncated/.test(log.log));
+const comment = "long_running_aggregation";
+const awaitShell =
+ startParallelShell(funWithArgs(startLongRunningAggregation, coll.getName(), comment));
-res.inprog.forEach((op) => {
- assert.commandWorked(db.killOp(op.opid));
-});
+const filter = {
+ ns: coll.getFullName(),
+ "command.comment": comment,
-serverCommandTest();
-
-// Test that the db.currentOp() shell helper does not truncate ops.
-const shellHelperTest = startShellWithOp("currentOp_shell");
-res = db.currentOp({"ns": "test.currentOp_cursor", "command.comment": "currentOp_shell"});
-
-if (FixtureHelpers.isMongos(db) && FixtureHelpers.isSharded(coll)) {
- assert(res.inprog.length >= 1, res);
- res.inprog.forEach((result) => {
- assert.eq(result.op, "getmore", result);
- assert(!result.cursor.originatingCommand.hasOwnProperty("$truncated"), result);
- });
-} else {
- assert.eq(res.inprog.length, 1, res);
- assert(!res.inprog[0].command.hasOwnProperty("$truncated"), res);
-}
+ // On the replica set endpoint, currentOp reports both router and shard operations. So filter
+ // out one of them.
+ role: TestData.testingReplicaSetEndpoint ? "ClusterRole{router}" : {$exists: false}
+};
-res.inprog.forEach((op) => {
- assert.commandWorked(db.killOp(op.opid));
+// The 'currentOp' shell helper should _not_ truncate the command.
+const operationsViaCurrentOpShellHelper = awaitOperations(function() {
+ return db.currentOp(filter).inprog;
});
-
-shellHelperTest();
+assert(operationsViaCurrentOpShellHelper.every(op => {
+ const command = getCommandFromCurrentOpEntry(op);
+ return !("$truncated" in command) && command.aggregate == coll.getName();
+}),
+ operationsViaCurrentOpShellHelper);
+
+// Finish the test by killing the long-running aggregation pipeline and joining the parallel shell
+// that launched it.
+for (let op of operationsViaCurrentOpShellHelper) {
+ assert.commandWorked(db.killOp(op.opid));
+}
+awaitShell();
})();
diff --git a/jstests/core/elemmatch_index.js b/jstests/core/elemmatch_index.js
index a1941620a48..f0aaf35cdba 100644
--- a/jstests/core/elemmatch_index.js
+++ b/jstests/core/elemmatch_index.js
@@ -80,6 +80,12 @@ assertIndexResults(coll, {a: {$elemMatch: {"": {$elemMatch: {"b.c": "x"}}}}}, fa
// Tests $elemMatch with supporting index and no path components that are empty strings.
assertIndexResults(coll, {a: {$elemMatch: {"b.c": "x"}}}, true, 1);
assertIndexResults(coll, {a: {$all: [{$elemMatch: {"b.c": "x"}}]}}, true, 1);
+
+// Tests that $elemMatch with path components that are empty strings are correctly handled in the
+// plan enumerator in case if an index is used on another predicate of the query.
+assertIndexResults(coll, {$and: [{"a.b.c": "x"}, {"": {$elemMatch: {"a.b.c": "x"}}}]}, true, 0);
+
+assertIndexResults(coll, {$and: [{"a.b.c": "x"}, {"a.b.c": {$elemMatch: {"": "x"}}}]}, true, 0);
})();
(function() {
diff --git a/jstests/core/empty_ts.js b/jstests/core/empty_ts.js
new file mode 100644
index 00000000000..926de0fd790
--- /dev/null
+++ b/jstests/core/empty_ts.js
@@ -0,0 +1,134 @@
+/**
+ * Test how inserts and updates behave with "Timestamp(0,0)" values.
+ *
+ * @tags: [
+ * requires_fcv_60,
+ * ]
+ */
+(function() {
+"use strict";
+
+const coll = db.jstests_core_empty_ts;
+const emptyTs = Timestamp(0, 0);
+
+coll.drop();
+
+// Insert several documents. For the first document inserted (_id=101), the empty timestamp value
+// in field "a" should get replaced with the current timestamp.
+assert.commandWorked(coll.insert({_id: 101, a: emptyTs}));
+assert.commandWorked(coll.insert({_id: 102, a: 1}));
+assert.commandWorked(coll.insert({_id: 103, a: 2}));
+assert.commandWorked(coll.insert({_id: 106, a: 3}));
+assert.commandWorked(coll.insert({_id: 107, a: 4}));
+assert.commandWorked(coll.insert({_id: 108, a: 5}));
+assert.commandWorked(coll.insert({_id: 109, a: 6}));
+assert.commandWorked(coll.insert({_id: 110, a: 7}));
+assert.commandWorked(coll.insert({_id: 111, a: 8}));
+
+// Use a replacement-style update to update _id=102. This should result in field "a" being set to
+// the current timestamp.
+//
+// This is an example of how the special "empty timestamp" behavior works with a replacement-style
+// update.
+assert.commandWorked(coll.update({_id: 102}, {a: emptyTs}));
+
+// Use a replacement-style findAndModify to update _id=103. This should result in field "a" being
+// set to the current timestamp.
+let findAndModifyResult = coll.findAndModify({query: {_id: 103}, update: {a: emptyTs}});
+assert.eq(findAndModifyResult, {_id: 103, a: 2});
+
+// Do a replacement-style update to add a new document with _id=104. This should result in field "a"
+// being set to the current timestamp.
+assert.commandWorked(coll.update({_id: 104}, {a: emptyTs}, {upsert: true}));
+
+// Do a replacement-style findAndModify to add a new document with _id=105. This should result in
+// field "a" being set to the current timestamp.
+findAndModifyResult = coll.findAndModify({query: {_id: 105}, update: {a: emptyTs}, upsert: true});
+assert.eq(findAndModifyResult, null);
+
+// For the rest of the commands below, the empty timestamp values stored in field "a" should be
+// preserved as-is.
+
+// Do an update-operator-style update to update _id=106.
+assert.commandWorked(coll.update({_id: 106}, {$set: {a: emptyTs}}));
+
+// Do an update-operator-style findAndModify to update _id=107.
+findAndModifyResult = coll.findAndModify({query: {_id: 107}, update: {$set: {a: emptyTs}}});
+assert.eq(findAndModifyResult, {_id: 107, a: 4});
+
+// Do a pipeline-style update to update _id=108.
+assert.commandWorked(coll.update({_id: 108}, [{$addFields: {a: emptyTs}}]));
+
+// Do a pipeline-style findAndModify to update _id=109.
+findAndModifyResult = coll.findAndModify({query: {_id: 109}, update: [{$addFields: {a: emptyTs}}]});
+assert.eq(findAndModifyResult, {_id: 109, a: 6});
+
+// Do a pipeline-style update with $internalApplyOplogUpdate to update _id=110.
+assert.commandWorked(coll.update(
+ {_id: 110}, [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}]));
+
+// Do a pipeline-style findAndModify with $internalApplyOplogUpdate to update _id=111.
+findAndModifyResult = coll.findAndModify({
+ query: {_id: 111},
+ update: [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}]
+});
+assert.eq(findAndModifyResult, {_id: 111, a: 8});
+
+// Do an update-operator-style update to add a new document with _id=112.
+assert.commandWorked(coll.update({_id: 112}, {$set: {a: emptyTs}}, {upsert: true}));
+
+// Do an update-operator-style findAndModify to add a new document with _id=113.
+findAndModifyResult =
+ coll.findAndModify({query: {_id: 113}, update: {$set: {a: emptyTs}}, upsert: true});
+assert.eq(findAndModifyResult, null);
+
+// Do a pipeline-style update to add a new document with _id=114.
+assert.commandWorked(coll.update({_id: 114}, [{$addFields: {a: emptyTs}}], {upsert: true}));
+
+// Do a pipeline-style findAndModify to add a new document with _id=115.
+findAndModifyResult =
+ coll.findAndModify({query: {_id: 115}, update: [{$addFields: {a: emptyTs}}], upsert: true});
+assert.eq(findAndModifyResult, null);
+
+// Do a pipline-style update with $internalApplyOplogUpdate to add a new document _id=116.
+assert.commandWorked(
+ coll.update({_id: 116},
+ [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}],
+ {upsert: true}));
+
+// Do pipeline-style findAndModify with $internalApplyOplogUpdate to add a new document _id=117.
+findAndModifyResult = coll.findAndModify({
+ query: {_id: 117},
+ update: [{$_internalApplyOplogUpdate: {oplogUpdate: {$v: 2, diff: {i: {a: emptyTs}}}}}],
+ upsert: true
+});
+assert.eq(findAndModifyResult, null);
+
+// Verify that all the insert, update, and findAndModify commands behaved the way we expected.
+for (let i = 101; i <= 117; ++i) {
+ let result = coll.findOne({_id: i});
+
+ if (i >= 106) {
+ assert.eq(tojson(result.a), tojson(emptyTs), "_id=" + i);
+ } else {
+ assert.neq(tojson(result.a), tojson(emptyTs), "_id=" + i);
+ }
+}
+
+// Insert a document with _id=Timestamp(0,0).
+assert.commandWorked(coll.insert({_id: emptyTs, a: 9}));
+
+// Verify the document we just inserted can be retrieved using the filter "{_id: Timestamp(0,0)}".
+let result = coll.findOne({_id: emptyTs});
+assert.eq(tojson(result._id), tojson(emptyTs), "_id=" + tojson(emptyTs));
+assert.eq(tojson(result.a), tojson(9), "_id=" + tojson(emptyTs));
+
+// Do a replacement-style update on the document.
+assert.commandWorked(coll.update({_id: emptyTs}, {_id: emptyTs, a: emptyTs}));
+
+// Verify the document we just updated can still be retrieved using "{_id: Timestamp(0,0)}", and
+// verify that field "a" was set to the current timestamp.
+result = coll.findOne({_id: emptyTs});
+assert.eq(tojson(result._id), tojson(emptyTs), "_id=" + tojson(emptyTs));
+assert.neq(tojson(result.a), tojson(emptyTs), "_id=" + tojson(emptyTs));
+}());
diff --git a/jstests/core/explain_agg_write_concern.js b/jstests/core/explain_agg_write_concern.js
index 9ff556489fa..ec246140abe 100644
--- a/jstests/core/explain_agg_write_concern.js
+++ b/jstests/core/explain_agg_write_concern.js
@@ -5,6 +5,7 @@
// assumes_unsharded_collection,
// assumes_write_concern_unchanged,
// does_not_support_stepdowns,
+// references_foreign_collection,
// requires_non_retryable_commands,
// ]
diff --git a/jstests/core/explain_skip.js b/jstests/core/explain_skip.js
new file mode 100644
index 00000000000..b6e01d6a436
--- /dev/null
+++ b/jstests/core/explain_skip.js
@@ -0,0 +1,28 @@
+/**
+ * Tests that explain reports the skip stage statistics correctly.
+ *
+ * @tags: [
+ * assumes_unsharded_collection,
+ * assumes_against_mongod_not_mongos,
+ * ]
+ */
+load("jstests/libs/analyze_plan.js"); // For getWinningPlan.
+
+var coll = db.explain_skip;
+coll.drop();
+
+assert.commandWorked(coll.insert({a: 1, b: 1}));
+assert.commandWorked(coll.insert({a: 1, b: 2}));
+assert.commandWorked(coll.insert({a: 1, b: 3}));
+assert.commandWorked(coll.insert({a: 2, b: 1}));
+assert.commandWorked(coll.insert({a: 2, b: 2}));
+assert.commandWorked(coll.insert({a: 2, b: 3}));
+
+assert.commandWorked(coll.createIndex({a: 1}));
+assert.commandWorked(coll.createIndex({b: 1}));
+
+var explain = coll.find({a: 1, b: 1}).sort({_id: 1}).skip(5).explain();
+var winningPlan = getWinningPlan(explain.queryPlanner);
+var skipStage = getPlanStage(winningPlan, "SKIP");
+assert.neq(null, skipStage, explain);
+assert.eq(5, skipStage.skipAmount, explain);
diff --git a/jstests/core/fts_mix.js b/jstests/core/fts_mix.js
index 5942a85ec2c..f8bf850fb29 100644
--- a/jstests/core/fts_mix.js
+++ b/jstests/core/fts_mix.js
@@ -1,4 +1,3 @@
-
(function() {
load("jstests/libs/fts.js");
load("jstests/aggregation/extras/utils.js"); // For resultsEq.
diff --git a/jstests/core/index_filter_commands.js b/jstests/core/index_filter_commands.js
index 6c5ff8920c8..6e5f5c5726a 100644
--- a/jstests/core/index_filter_commands.js
+++ b/jstests/core/index_filter_commands.js
@@ -30,7 +30,8 @@
* assumes_read_preference_unchanged,
* assumes_unsharded_collection,
* does_not_support_stepdowns,
- * requires_fcv_60
+ * requires_fcv_60,
+ * references_foreign_collection,
* ]
*/
diff --git a/jstests/core/index_stats.js b/jstests/core/index_stats.js
index ce5c9901e48..b0abd2e35a1 100644
--- a/jstests/core/index_stats.js
+++ b/jstests/core/index_stats.js
@@ -12,6 +12,7 @@
// # Tenant migrations passthrough suites automatically retry operations on TenantMigrationAborted
// # errors.
// tenant_migration_incompatible,
+// references_foreign_collection,
// ]
(function() {
diff --git a/jstests/core/json_schema/json_schema.js b/jstests/core/json_schema/json_schema.js
index c1e06344498..a69db77c60d 100644
--- a/jstests/core/json_schema/json_schema.js
+++ b/jstests/core/json_schema/json_schema.js
@@ -1,7 +1,7 @@
// listCollections tests expect that a collection is not implicitly created after a drop.
// @tags: [
// assumes_no_implicit_collection_creation_after_drop,
-// requires_non_retryable_commands,
+// requires_non_retryable_commands
// ]
/**
diff --git a/jstests/core/json_schema/misc_validation.js b/jstests/core/json_schema/misc_validation.js
index 58e76fc0d68..569d98be258 100644
--- a/jstests/core/json_schema/misc_validation.js
+++ b/jstests/core/json_schema/misc_validation.js
@@ -19,6 +19,7 @@
* requires_replication,
* # This test depends on hardcoded database name equality.
* tenant_migration_incompatible,
+ * references_foreign_collection,
* ]
*/
(function() {
diff --git a/jstests/core/negated_geo_queries.js b/jstests/core/negated_geo_queries.js
new file mode 100644
index 00000000000..e6c0cfbbf03
--- /dev/null
+++ b/jstests/core/negated_geo_queries.js
@@ -0,0 +1,93 @@
+/**
+ * With a normal ascending index on a geospatial field, {$not: {$geoWithin: <>}} and
+ * {$not: {$geoIntersects: <>}} queries should return proper results. Previously, those queries
+ * failed due to attempting to build geospatial index bounds on the non-geo index. See SERVER-92193
+ * for more details.
+ */
+(function() {
+"use strict";
+
+const coll = db.negated_geo_queries;
+coll.drop();
+
+assert.commandWorked(coll.insert(
+ {loc: {type: "Polygon", coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]}, a: 0}));
+assert.commandWorked(coll.insert(
+ {loc: {type: "Polygon", coordinates: [[[30, 0], [30, 1], [32, 1], [32, 0], [30, 0]]]}, a: 0}));
+assert.commandWorked(coll.insert(
+ {loc: {type: "Polygon", coordinates: [[[-55, 10], [-35, 15], [-45, 10], [-55, 10]]]}, a: 1}));
+
+const targetPolygonWithin = {
+ type: "Polygon",
+ coordinates: [[[-5, -5], [5, -5], [5, 5], [-5, 5], [-5, -5]]]
+};
+
+const targetLineIntersect = {
+ type: "LineString",
+ coordinates: [[29, -1], [31, 1]]
+};
+
+function runTest() {
+ // Tests $geoWithin.
+ let geoQuery = {loc: {$geoWithin: {$geometry: targetPolygonWithin}}};
+ let res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 1);
+
+ // Tests $geoWithin under a $not.
+ geoQuery = {loc: {$not: {$geoWithin: {$geometry: targetPolygonWithin}}}};
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 2);
+
+ // Tests $geoIntersects.
+ geoQuery = {loc: {$geoIntersects: {$geometry: targetLineIntersect}}};
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 1);
+
+ // Tests $geoIntersects under a $not.
+ geoQuery = {loc: {$not: {$geoIntersects: {$geometry: targetLineIntersect}}}};
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 2);
+
+ // Tests an $and of the negated $geoWithin and negated $geoIntersects.
+ geoQuery = {
+ $and: [
+ {loc: {$not: {$geoWithin: {$geometry: targetPolygonWithin}}}},
+ {loc: {$not: {$geoIntersects: {$geometry: targetLineIntersect}}}}
+ ]
+ };
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 1);
+
+ // Tests the same logic as above written with a $nor.
+ geoQuery = {
+ $nor: [
+ {loc: {$geoWithin: {$geometry: targetPolygonWithin}}},
+ {loc: {$geoIntersects: {$geometry: targetLineIntersect}}}
+ ]
+ };
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 1);
+
+ // Tests an $and of the negated $geoWithin and an expression on a non-geo field.
+ geoQuery = {$and: [{loc: {$not: {$geoWithin: {$geometry: targetPolygonWithin}}}}, {a: 0}]};
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 1);
+
+ // Tests an $and of the negated $geoIntersects and an expression on a non-geo field.
+ geoQuery = {$and: [{loc: {$not: {$geoIntersects: {$geometry: targetLineIntersect}}}}, {a: 0}]};
+ res = coll.find(geoQuery);
+ assert.eq(res.itcount(), 1);
+}
+
+// Run test with just a simple btree index.
+assert.commandWorked(coll.createIndex({loc: 1}));
+runTest();
+
+// Run test with a simple btree index and a geo index.
+assert.commandWorked(coll.createIndex({loc: "2dsphere"}));
+runTest();
+
+// Run test with just a geo index.
+assert.commandWorked(coll.dropIndex({loc: 1}));
+runTest();
+})();
diff --git a/jstests/core/notablescan.js b/jstests/core/notablescan.js
deleted file mode 100644
index e99ecd013e8..00000000000
--- a/jstests/core/notablescan.js
+++ /dev/null
@@ -1,86 +0,0 @@
-// check notablescan mode
-//
-// @tags: [
-// # The test runs commands that are not allowed with security token: setParameter.
-// not_allowed_with_security_token,
-// assumes_against_mongod_not_mongos,
-// # This test attempts to perform read operations after having enabled the notablescan server
-// # parameter. The former operations may be routed to a secondary in the replica set, whereas the
-// # latter must be routed to the primary.
-// assumes_read_preference_unchanged,
-// assumes_superuser_permissions,
-// does_not_support_stepdowns,
-// # Server parameters are stored in-memory only so are not transferred onto the recipient. This
-// # test sets the server parameter "notablescan" to force the node to not execute queries that
-// # require a collection scan and return an error.
-// tenant_migration_incompatible,
-// ]
-
-(function() {
-load("jstests/libs/analyze_plan.js");
-load("jstests/libs/collection_drop_recreate.js");
-
-function checkError(err) {
- assert.includes(err.toString(), "'notablescan'");
-}
-
-const colName = jsTestName();
-let coll = db.getCollection(colName);
-coll.drop();
-
-assert.commandWorked(db.adminCommand({setParameter: 1, notablescan: true}));
-
-{
- if (0) {
- // TODO: SERVER-2222 This should actually throw an error as it performs a collection
- // scan.
- assert.throws(function() {
- coll.find({a: 1}).toArray();
- });
- }
-
- coll.insert({a: 1});
- let err = assert.throws(function() {
- coll.count({a: 1});
- });
- checkError(err);
-
- // TODO: SERVER-2222 This should actually throw an error as it performs a collection scan.
- assert.eq(1, coll.find({}).itcount());
-
- err = assert.throws(function() {
- coll.find({a: 1}).toArray();
- });
- checkError(err);
-
- err = assert.throws(function() {
- coll.find({a: 1}).hint({$natural: 1}).toArray();
- });
- assert.includes(err.toString(), "$natural");
- checkError(err);
-
- coll.createIndex({a: 1});
- assert.eq(0, coll.find({a: 1, b: 1}).itcount());
- assert.eq(1, coll.find({a: 1, b: null}).itcount());
-}
-
-{ // Run the testcase with a clustered index.
- assertDropAndRecreateCollection(db, colName, {clusteredIndex: {key: {_id: 1}, unique: true}});
- coll = db.getCollection(colName);
- assert.commandWorked(coll.insert({_id: 22}));
- assert.eq(1, coll.find({_id: 22}).itcount());
- let plan = coll.find({_id: 22}).explain();
- // Make sure the plan has a clustered index scan.
- assert(isClusteredIxscan(db, plan));
-
- // Make sure the same works with an aggregate.
- assert.eq(1, coll.aggregate([{$match: {_id: 22}}]).itcount());
- plan = coll.explain().aggregate([{$match: {_id: 22}}]);
- // Make sure the plan has a clustered index scan.
- assert(isClusteredIxscan(db, plan));
- assert.commandWorked(
- db.runCommand({aggregate: colName, pipeline: [{$match: {_id: 22}}], cursor: {}}));
-}
-// Set it back to the original value.
-assert.commandWorked(db.adminCommand({setParameter: 1, notablescan: false}));
-})();
diff --git a/jstests/core/opcounters_write_cmd.js b/jstests/core/opcounters_write_cmd.js
index 6f74185e9f7..9e0d68cd928 100644
--- a/jstests/core/opcounters_write_cmd.js
+++ b/jstests/core/opcounters_write_cmd.js
@@ -2,6 +2,7 @@
// @tags: [
// uses_multiple_connections,
// assumes_standalone_mongod,
+// inspects_command_opcounters,
// ]
// Legacy write mode test also available at jstests/gle.
diff --git a/jstests/core/plan_cache_list_plans.js b/jstests/core/plan_cache_list_plans.js
index ca571a9e39e..802a25635e0 100644
--- a/jstests/core/plan_cache_list_plans.js
+++ b/jstests/core/plan_cache_list_plans.js
@@ -12,6 +12,7 @@
// assumes_unsharded_collection,
// does_not_support_stepdowns,
// inspects_whether_plan_cache_entry_is_active,
+// references_foreign_collection,
// ]
(function() {
diff --git a/jstests/core/profile_agg.js b/jstests/core/profile_agg.js
index d6611fa98ab..b1ce28dd696 100644
--- a/jstests/core/profile_agg.js
+++ b/jstests/core/profile_agg.js
@@ -1,6 +1,7 @@
// @tags: [
// does_not_support_stepdowns,
// requires_profiling,
+// references_foreign_collection,
// ]
// Confirms that profiled aggregation execution contains all expected metrics with proper values.
diff --git a/jstests/core/query/find_all_values_at_path_expression.js b/jstests/core/query/find_all_values_at_path_expression.js
new file mode 100644
index 00000000000..22a26c593ed
--- /dev/null
+++ b/jstests/core/query/find_all_values_at_path_expression.js
@@ -0,0 +1,33 @@
+/**
+ * Tests that $_internalFindAllValuesAtPath asserts when provided a non-string input.
+ */
+const coll = db.find_all_values_at_path_expression;
+coll.drop();
+
+// In query stats, this fails with a different error code.
+const errorCodes = [9567004, 9423101];
+
+// Insert some documents because running aggregation on a non-existent collection on mongos will
+// return empty instead of erroring.
+let documents = [{a: 4}, {a: 5, b: 1}, {a: 0, b: 1}, {a: 0, b: 1}, {a: 2, b: {c: 1}}];
+assert.commandWorked(coll.insert(documents));
+assert.commandFailedWithCode(db.runCommand({
+ aggregate: coll.getName(),
+ pipeline: [
+ {$replaceRoot: {newRoot: {$_internalFindAllValuesAtPath: null}}},
+ {$unwind: {path: "$_internalUnwoundField", preserveNullAndEmptyArrays: true}},
+ {$group: {_id: null, distinct: {$addToSet: "$<key>"}}}
+ ],
+ cursor: {}
+}),
+ errorCodes);
+assert.commandFailedWithCode(db.runCommand({
+ aggregate: coll.getName(),
+ pipeline: [
+ {$replaceRoot: {newRoot: {$_internalFindAllValuesAtPath: 12345}}},
+ {$unwind: {path: "$_internalUnwoundField", preserveNullAndEmptyArrays: true}},
+ {$group: {_id: null, distinct: {$addToSet: "$<key>"}}}
+ ],
+ cursor: {}
+}),
+ errorCodes);
diff --git a/jstests/core/query/internal_strip_invalid_assignment.js b/jstests/core/query/internal_strip_invalid_assignment.js
index 4385e6730a4..b8868c6e951 100644
--- a/jstests/core/query/internal_strip_invalid_assignment.js
+++ b/jstests/core/query/internal_strip_invalid_assignment.js
@@ -3,6 +3,11 @@
* assertion failure when we strip its index assignment from special indexes.
*
* Originally intended to reproduce SERVER-82717.
+ *
+ * @tags: [
+ * # Explain command does not support read concerns other than local.
+ * assumes_read_concern_local,
+ * ]
*/
(function() {
load('jstests/libs/analyze_plan.js'); // for planHasStage
diff --git a/jstests/core/rename_collection_system_db.js b/jstests/core/rename_collection_system_db.js
index 8d46ad27a04..e9e166914c0 100644
--- a/jstests/core/rename_collection_system_db.js
+++ b/jstests/core/rename_collection_system_db.js
@@ -16,10 +16,9 @@ systemUsers.drop();
coll.drop();
coll.insert({});
-// system.foo isn't in the allowlist so it can't be renamed to or from
+// system.foo and system.users aren't in the allowlist so they can't be renamed to or from
assert.commandFailed(coll.renameCollection(systemFoo.getName()));
assert.commandFailed(systemFoo.renameCollection(coll.getName()));
-// system.users is allowlisted so these should work
-assert.commandWorked(coll.renameCollection(systemUsers.getName()));
-assert.commandWorked(systemUsers.renameCollection(coll.getName()));
+assert.commandFailed(coll.renameCollection(systemUsers.getName()));
+assert.commandFailed(systemUsers.renameCollection(coll.getName()));
diff --git a/jstests/core/timeseries/libs/timeseries.js b/jstests/core/timeseries/libs/timeseries.js
index d9e22926520..540e0bcea55 100644
--- a/jstests/core/timeseries/libs/timeseries.js
+++ b/jstests/core/timeseries/libs/timeseries.js
@@ -11,6 +11,20 @@ var TimeseriesTest = class {
return FeatureFlagUtil.isEnabled(conn, "TimeseriesBucketCompression");
}
+ static bucketsMayHaveMixedSchemaData(coll) {
+ const catalog = coll.aggregate([{$listCatalog: {}}]).toArray()[0];
+ const tsMixedSchemaOptionNewFormat = catalog.md.options.storageEngine &&
+ catalog.md.options.storageEngine.wiredTiger &&
+ catalog.md.options.storageEngine.wiredTiger.configString;
+ // TODO SERVER-92533 Simplify once SERVER-91195 is backported to all supported branches
+ if (tsMixedSchemaOptionNewFormat !== undefined) {
+ return tsMixedSchemaOptionNewFormat ==
+ "app_metadata=(timeseriesBucketsMayHaveMixedSchemaData=true)";
+ } else {
+ return catalog.md.timeseriesBucketsMayHaveMixedSchemaData;
+ }
+ }
+
/**
* Returns whether time-series updates and deletes are supported.
*/
diff --git a/jstests/core/timeseries/timeseries_collmod.js b/jstests/core/timeseries/timeseries_collmod.js
index 69fd7dbeb82..07c9166b632 100644
--- a/jstests/core/timeseries/timeseries_collmod.js
+++ b/jstests/core/timeseries/timeseries_collmod.js
@@ -19,6 +19,17 @@ assert.commandWorked(
db.createCollection(collName, {timeseries: {timeField: "time", granularity: 'seconds'}}));
assert.commandWorked(coll.createIndex({"time": 1}));
+// Setting prepareUnique should return an error on a time-series collection.
+assert.commandFailedWithCode(
+ db.runCommand(
+ {"collMod": collName, "index": {"keyPattern": {"time": 1}, "prepareUnique": true}}),
+ ErrorCodes.InvalidOptions);
+
+assert.commandFailedWithCode(
+ db.runCommand(
+ {"collMod": collName, "index": {"keyPattern": {"time": 1}, "prepareUnique": false}}),
+ ErrorCodes.InvalidOptions);
+
// Tries to convert a time-series secondary index to TTL index.
assert.commandFailedWithCode(
db.runCommand(
@@ -56,4 +67,4 @@ assert.commandWorked(db.runCommand({"collMod": collName, "expireAfterSeconds": 6
// Successfully sets the granularity for a time-series collection.
assert.commandWorked(
db.runCommand({"collMod": collName, "timeseries": {"granularity": "minutes"}}));
-})(); \ No newline at end of file
+})();
diff --git a/jstests/core/timeseries/timeseries_computed_field.js b/jstests/core/timeseries/timeseries_computed_field.js
new file mode 100644
index 00000000000..fab03d1a3b0
--- /dev/null
+++ b/jstests/core/timeseries/timeseries_computed_field.js
@@ -0,0 +1,856 @@
+/**
+ * Test use of computed fields in aggregations on time series collections.
+ * @tags: [
+ * requires_timeseries,
+ * does_not_support_stepdowns,
+ * directly_against_shardsvrs_incompatible,
+ * # "Explain of a resolved view must be executed by mongos"
+ * directly_against_shardsvrs_incompatible,
+ * # Some suites use mixed-binary cluster setup where some nodes might have the flag enabled while
+ * # others -- not. For this test we need control over whether the flag is set on the node that
+ * # ends up executing the query.
+ * assumes_standalone_mongod
+ * ]
+ */
+load("jstests/core/timeseries/libs/timeseries.js");
+
+TimeseriesTest.run((insert) => {
+ const datePrefix = 1680912440;
+
+ let coll = db.timeseries_computed_field;
+ const bucketsColl = db.getCollection('system.buckets.' + coll.getName());
+
+ const timeFieldName = 'time';
+ const metaFieldName = 'measurement';
+
+ coll.drop();
+ assert.commandWorked(db.createCollection(coll.getName(), {
+ timeseries: {timeField: timeFieldName, metaField: metaFieldName},
+ }));
+ assert.contains(bucketsColl.getName(), db.getCollectionNames());
+
+ insert(coll, {
+ _id: 0,
+ [timeFieldName]: new Date(datePrefix + 100), // ISODate("1970-01-20T10:55:12.540Z")
+ [metaFieldName]: "cpu",
+ topLevelScalar: 123,
+ topLevelScalarDouble: 123.8778645,
+ topLevelLargeNumber: 12345678912345,
+ topLevelArray: [1, 2, 3, 4],
+ arrOfObj: [{x: 1}, {x: 2}, {x: 3}, {x: 4}],
+ obj: {a: 123},
+ length: 0,
+ });
+ insert(coll, {
+ _id: 1,
+ [timeFieldName]: new Date(datePrefix + 200), // ISODate("1970-01-20T10:55:12.640Z")
+ [metaFieldName]: "cpu",
+ topLevelScalar: 456,
+ topLevelScalarDouble: 546.76858699,
+ topLevelArray: [101, 102, 103, 104],
+ arrOfObj: [{x: 101}, {x: 102}, {x: 103}, {x: 104}],
+ obj: {a: 456},
+ length: 23,
+ });
+ // Insert a document that will be placed in a different bucket.
+ insert(coll, {
+ _id: 2,
+ [timeFieldName]: new Date(datePrefix + 300),
+ [metaFieldName]: "gpu",
+ length: -2,
+ });
+
+ // Computing a field on a dotted path which is an array, then grouping on it. Note that the
+ // semantics for setting a computed field on a dotted array path are particularly strange, but
+ // should be preserved for backwards compatibility.
+ {
+ const res = coll.aggregate([
+ {$addFields: {"arrOfObj.x": {$trim: {input: "test string"}}}},
+ {$match: {topLevelScalar: {$gte: 0}}},
+ {$group: {_id: null, max: {$max: "$arrOfObj.x"}}}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].max, ["test string", "test string", "test string", "test string"], res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {$addFields: {"arrOfObj.x": {$add: ["$topLevelScalar", 1]}}},
+ {$match: {topLevelScalar: {$gte: 0}}},
+ {$group: {_id: null, max: {$max: "$arrOfObj.x"}}}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].max, [457, 457, 457, 457], res);
+ }
+
+ // Computing a field and then filtering by it.
+ {
+ const res = coll.aggregate([
+ {$addFields: {"arrOfObj.x": {$trim: {input: "test string"}}}},
+ {$match: {"arrOfObj.x": "test string"}},
+ {$group: {_id: null, max: {$max: "$arrOfObj.x"}}}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].max, ["test string", "test string", "test string", "test string"], res);
+ }
+
+ {
+ // Computing a field based on a dotted path which does not traverse arrays.
+ const res = coll.aggregate([
+ {$addFields: {"computedA": {$add: ["$obj.a", 1]}}},
+ // Only one document should have a value where obj.a was 457
+ // (456 + 1).
+ {$match: {"computedA": 457}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ // Include an unnecessary computed field before counting the number of documents.
+ const res = coll.aggregate([{$addFields: {"a": 1}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 3, res);
+ }
+
+ // mathematical expressions
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$add: ["$topLevelScalar", 1]}}},
+ {$match: {"computedA": 457}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$subtract: ["$topLevelScalar", 1]}}},
+ {$match: {"computedA": 455}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$multiply: ["$topLevelScalar", 10]}}},
+ {$match: {"computedA": 4560}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$divide: ["$topLevelScalar", 2]}}},
+ {$match: {"computedA": 228}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$add: [1, "$topLevelScalar"]}}},
+ {$match: {"computedA": 457}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$subtract: [200, "$topLevelScalar"]}}},
+ {$match: {"computedA": 77}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$multiply: [10, "$topLevelScalar"]}}},
+ {$match: {"computedA": 4560}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$divide: [4560, "$topLevelScalar"]}}},
+ {$match: {"computedA": 10}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$add: ["$topLevelScalar", "$topLevelScalar"]}}},
+ {$match: {"computedA": 912}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$subtract: ["$topLevelScalar", "$topLevelScalar"]}}},
+ {$match: {"computedA": 0}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$multiply: ["$topLevelScalar", "$topLevelScalar"]}}},
+ {$match: {"computedA": 15129}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$divide: ["$topLevelScalar", "$topLevelScalar"]}}},
+ {$match: {"computedA": 1}},
+ {$count: "count"}
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ let pipeline = [
+ {
+ $addFields: {
+ "hourDiff": {
+ $dateDiff: {
+ "startDate": "$time",
+ "endDate": new Date("1970-01-21"),
+ "unit": "hour"
+ }
+ }
+ }
+ },
+ {$match: {"hourDiff": {$gte: 12}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "open": {"$first": "$topLevelScalar"},
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "open": 123}],
+ coll.aggregate(pipeline).toArray());
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {"mt": {$multiply: ["$topLevelScalar", 10]}}},
+ {$match: {"mt": {$gte: 2000}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "open": {"$first": "$topLevelScalar"},
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "open": 456}],
+ coll.aggregate(pipeline).toArray());
+ }
+
+ {
+ let pipeline = [
+ {
+ $addFields: {
+ "hourDiff": {
+ $dateDiff: {
+ "startDate": "$time",
+ "endDate": new Date("1970-01-21"),
+ "unit": "hour"
+ }
+ }
+ }
+ },
+ {$match: {$or: [{topLevelScalar: {$gt: 200, $lt: 800}}, {hourDiff: {$gte: 12}}]}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "open": {"$first": "$topLevelScalar"},
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "open": 123}],
+ coll.aggregate(pipeline).toArray());
+ }
+
+ {
+ // Try a project stage which adds and remove subfields.
+ const res = coll.aggregate([
+ {$project: {"obj.newField": "$topLevelScalar"}},
+ {$project: {"_id": 0, "obj.a": 0}}
+ ])
+ .toArray();
+ assert.eq(res.length, 3, res);
+ assert.eq(res[0], {"obj": {"newField": 123}}, res);
+ assert.eq(res[1], {"obj": {"newField": 456}}, res);
+ assert.eq(res[2], {"obj": {}}, res);
+ }
+
+ {
+ // Try a replaceRoot stage which remove all fields.
+ const res = coll.aggregate([
+ {$match: {"time": {$gte: new Date(datePrefix + 200)}}},
+ {$addFields: {}},
+ {$project: {"measurement0": {$floor: "$topLevelScalar"}}},
+ {$replaceRoot: {newRoot: {}}}
+ ])
+ .toArray();
+ assert.eq(res.length, 2, res);
+ assert.eq(res[0], {}, res);
+ assert.eq(res[1], {}, res);
+ }
+
+ {
+ // Try a $match that works on fields that are not projected.
+ const res =
+ coll.aggregate([
+ {$project: {"obj.a": 1}},
+ {$match: {$or: [{"topLevelScalar": {$gt: 10}}, {$expr: {$literal: true}}]}},
+ {$sort: {_id: 1}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 3, res);
+ }
+
+ {
+ const res = coll.aggregate([{
+ "$project": {
+ "t": {
+ "$dateDiff": {
+ "startDate": "$time",
+ "endDate": new Date(datePrefix + 150),
+ "unit": "millisecond"
+ }
+ }
+ }
+ }])
+ .toArray();
+ assert.eq(res.length, 3, res);
+ assert.docEq(res[0], {_id: 0, "t": NumberLong(50)}, res);
+ assert.docEq(res[1], {_id: 1, "t": NumberLong(-50)}, res);
+ assert.docEq(res[2], {_id: 2, "t": NumberLong(-150)}, res);
+ }
+
+ {
+ const res = coll.aggregate([{
+ "$project": {
+ "t": {
+ "$dateDiff": {
+ "startDate": new Date(datePrefix - 10),
+ "endDate": "$time",
+ "unit": "millisecond"
+ }
+ }
+ }
+ }])
+ .toArray();
+ assert.eq(res.length, 3, res);
+ assert.docEq(res[0], {_id: 0, "t": NumberLong(110)}, res);
+ assert.docEq(res[1], {_id: 1, "t": NumberLong(210)}, res);
+ assert.docEq(res[2], {_id: 2, "t": NumberLong(310)}, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {
+ $addFields: {
+ "computedField": {
+ $dateTrunc: {
+ date: "$time",
+ unit: "second",
+ }
+ }
+ }
+ },
+ {$match: {computedField: new Date(datePrefix - 440)}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 3, res);
+ }
+
+ {
+ const res = coll.aggregate([{$match: {topLevelScalar: {$exists: true}}}, {$count: "count"}])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([{$match: {topLevelScalar: {$lt: 456}}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([{$match: {topLevelScalar: {$lte: 456}}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([{$match: {topLevelScalar: {$gt: 123}}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([{$match: {topLevelScalar: {$gte: 123}}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([{$match: {topLevelScalar: {$eq: 123}}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([{$match: {topLevelScalar: {$ne: 123}}}, {$count: "count"}]).toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {
+ $match: {
+ $or: [
+ {"topLevelScalar": 123},
+ {"topLevelScalar": 456},
+ ]
+ }
+ },
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {$addFields: {"computedField": {$and: ["$topLevelScalar", true]}}},
+ {$match: {computedField: true}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {$addFields: {"computedField": {$and: ["$topLevelScalar", "$obj.a"]}}},
+ {$match: {computedField: true}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {$addFields: {"computedField": {$or: ["$topLevelScalar", false]}}},
+ {$match: {computedField: true}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {$addFields: {"computedField": {$or: ["$topLevelScalar", "$obj.a"]}}},
+ {$match: {computedField: true}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {$addFields: {"computedField": {$not: ["$topLevelScalar"]}}},
+ {$match: {computedField: true}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([
+ {$addFields: {"computedField": {$anyElementTrue: [["$topLevelScalar"]]}}},
+ {$match: {computedField: true}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 2, res);
+ }
+
+ {
+ const res =
+ coll.aggregate([
+ {$match: {length: {$ne: 0}}},
+ {$set: {"computedA": {$multiply: ["$topLevelScalar", "$topLevelScalar"]}}},
+ {$addFields: {"ratio": {$divide: ["$computedA", "$length"]}}},
+ {$match: {ratio: {$gt: 0}}},
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0]._id, 1, res);
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$round: ["$topLevelScalarDouble", 2]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "value": 670.65}], res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$round: ["$topLevelScalarDouble"]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "value": 671}], res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$round: [2, "$topLevelScalarDouble"]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ assert.throws(() => coll.aggregate(pipeline));
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$round: ["$topLevelScalarDouble", "$topLevelScalar"]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ assert.throws(() => coll.aggregate(pipeline));
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$trunc: ["$topLevelScalarDouble", 2]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "value": 670.63}], res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$trunc: ["$topLevelScalarDouble"]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ const res = coll.aggregate(pipeline).toArray();
+ assert.docEq([{"_id": {"time": ISODate("1970-01-20T10:55:00Z")}, "value": 669}], res);
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$trunc: [2, "$topLevelScalarDouble"]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ assert.throws(() => coll.aggregate(pipeline));
+ }
+ {
+ let pipeline = [
+ {$addFields: {"computedA": {$trunc: ["$topLevelScalarDouble", "$topLevelScalar"]}}},
+ {$match: {"computedA": {$gte: 100}}},
+ {
+ $group: {
+ "_id": {"time": {"$dateTrunc": {"date": "$time", "unit": "minute"}}},
+ "value": {$sum: "$computedA"},
+ }
+ }
+ ];
+
+ assert.throws(() => coll.aggregate(pipeline));
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {md: {$mod: ["$topLevelScalar", 5]}}},
+ {
+ $group: {
+ "_id": {$dateTrunc: {"date": "$time", "unit": "minute"}},
+ "total1": {$sum: "$md"},
+ "total2": {$sum: "$topLevelScalar"}
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": ISODate("1970-01-20T10:55:00Z"), "total1": 4, "total2": 579}],
+ coll.aggregate(pipeline).toArray());
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {md: {$mod: [500, "$topLevelScalar"]}}},
+ {
+ $group: {
+ "_id": {$dateTrunc: {"date": "$time", "unit": "minute"}},
+ "total1": {$sum: "$md"},
+ "total2": {$sum: "$topLevelScalar"}
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": ISODate("1970-01-20T10:55:00Z"), "total1": 52, "total2": 579}],
+ coll.aggregate(pipeline).toArray());
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {md: {$mod: ["$topLevelScalar", "$topLevelScalar"]}}},
+ {
+ $group: {
+ "_id": {$dateTrunc: {"date": "$time", "unit": "minute"}},
+ "total1": {$sum: "$md"},
+ "total2": {$sum: "$topLevelScalar"}
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": ISODate("1970-01-20T10:55:00Z"), "total1": 0, "total2": 579}],
+ coll.aggregate(pipeline).toArray());
+ }
+
+ {
+ let pipeline = [
+ {$addFields: {md: {$mod: ["$topLevelScalar", 5]}}},
+ {$match: {md: {$mod: [4, 1]}}},
+ {
+ $group: {
+ "_id": {$dateTrunc: {"date": "$time", "unit": "minute"}},
+ "total1": {$sum: "$md"},
+ "total2": {$sum: "$topLevelScalar"}
+ }
+ }
+ ];
+
+ assert.docEq([{"_id": ISODate("1970-01-20T10:55:00Z"), "total1": 1, "total2": 456}],
+ coll.aggregate(pipeline).toArray());
+ }
+ {
+ let pipeline = [{$match: {topLevelLargeNumber: {$mod: [1, 0]}}}, {$count: "count"}];
+
+ const res = coll.aggregate(pipeline).toArray();
+
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+ {
+ const res =
+ coll.aggregate([
+ {
+ $addFields: {
+ "computedField":
+ {$dateAdd: {startDate: "$time", unit: "millisecond", amount: 100}}
+ }
+ },
+ {$match: {computedField: new Date(datePrefix + 400)}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ const res = coll.aggregate([
+ {
+ $addFields: {
+ "computedField": {
+ $dateSubtract:
+ {startDate: "$time", unit: "millisecond", amount: 100}
+ }
+ }
+ },
+ {$match: {computedField: new Date(datePrefix)}},
+ {$count: "count"}
+ ])
+ .toArray();
+ assert.eq(res.length, 1, res);
+ assert.eq(res[0].count, 1, res);
+ }
+
+ {
+ // Test the case where a computed meta field is computed to missing.
+ const res =
+ coll.aggregate([{"$project": {"_id": 0, [metaFieldName]: "$$REMOVE"}}]).toArray();
+ assert.eq(res.length, coll.count(), res);
+ for (let doc of res) {
+ assert.eq(doc, {}, res);
+ }
+ }
+
+ {
+ // Test the case where a computed meta field is computed to missing because of a project
+ // out.
+ const res = coll.aggregate([
+ {"$project": {[metaFieldName]: 0}},
+ {"$project": {[metaFieldName]: "$" + metaFieldName}},
+ {$match: {a: 1}}
+ ])
+ .toArray();
+ assert.eq(res.length, 0, res);
+ }
+
+ {
+ // Test the case where a field is projected out and then projected back in.
+ const res = coll.aggregate([
+ {"$project": {"_id": 0, [metaFieldName]: 1}},
+ {"$project": {"_id": 0, [timeFieldName]: "$" + timeFieldName}},
+ {"$project": {"_id": 1, [timeFieldName]: 1}}
+ ])
+ .toArray();
+ assert.eq(res.length, coll.count(), res);
+ for (let doc of res) {
+ assert.eq(doc, {}, res);
+ }
+ }
+
+ {
+ // Test the case where a field is projected out and then projected back in.
+ const res = coll.aggregate([
+ {"$project": {"_id": 0, [metaFieldName]: 1}},
+ {"$project": {"_id": 0, [timeFieldName]: 1}}
+ ])
+ .toArray();
+ assert.eq(res.length, coll.count(), res);
+ for (let doc of res) {
+ assert.eq(doc, {}, res);
+ }
+ }
+
+ {
+ // Test the case where a field is projected out and then projected back in.
+ const res = coll.aggregate([
+ {"$project": {"_id": 0, [metaFieldName]: 0}},
+ {"$project": {"_id": 0, [metaFieldName]: 1}},
+ {"$project": {"_id": 0, [timeFieldName]: 1}}
+ ])
+ .toArray();
+ assert.eq(res.length, coll.count(), res);
+ for (let doc of res) {
+ assert.eq(doc, {}, res);
+ }
+ }
+});
diff --git a/jstests/core/timeseries/timeseries_filter_extended_range.js b/jstests/core/timeseries/timeseries_filter_extended_range.js
index 2b86e29c943..34018b3d2f4 100644
--- a/jstests/core/timeseries/timeseries_filter_extended_range.js
+++ b/jstests/core/timeseries/timeseries_filter_extended_range.js
@@ -1,6 +1,6 @@
/**
- * Test that find/match type queries work properly on dates ouside the 32 bit epoch range,
- * [1970-01-01 00:00:00 UTC - 2038-01-29 03:13:07 UTC].
+ * Test that find and match type queries work properly on dates outside the 32 bit epoch range,
+ * [1970-01-01 00:00:00 UTC - 2038-01-19 03:14:07 UTC].
*
* @tags: [
* # Refusing to run a test that issues an aggregation command with explain because it may
@@ -12,174 +12,301 @@
* does_not_support_transactions,
* # Explain of a resolved view must be executed by mongos.
* directly_against_shardsvrs_incompatible,
+ * requires_fcv_60
* ]
*/
(function() {
"use strict";
+
const timeFieldName = "time";
+const standardDocs = [
+ {[timeFieldName]: ISODate("1975-12-01")},
+ {[timeFieldName]: ISODate("1980-01-13")},
+ {[timeFieldName]: ISODate("2018-07-14")},
+ {[timeFieldName]: ISODate("2030-09-30")},
+];
+const beforeEpochDocs = [
+ {[timeFieldName]: ISODate("1969-12-31T23:00:59.001Z")},
+ {[timeFieldName]: ISODate("1969-12-31T23:59:59.001Z")} /* one millisecond before the epoch */,
+];
+const afterEpochDocs = [
+ // This date is one millisecond after the maximum (the largest 32 bit integer) number of seconds
+ // since the epoch.
+ {[timeFieldName]: ISODate("2038-01-19T03:14:07.001Z")},
+ {[timeFieldName]: ISODate("2050-01-20T03:14:00.003Z")}
+];
+const allExtendedRangeDocs = beforeEpochDocs.concat(afterEpochDocs);
/*
- * Creates a collection, populates it, runs the `query` and ensures that the result set
- * is equal to `results`.
- *
- * If overflow is set we create a document with dates above the 32 bit range (year 2040)
- * If underflow is set, we create a document with dates below the 32 bit range (year 1965)
+ * Creates a collection, populates it using the `standardDocs` and any dates passed in by
+ * `extendedRangeDocs`, runs the `query` and ensures that the result set is equal to `results`.
*/
-function runTest(underflow, overflow, query, results) {
- // Setup our DB & our collections.
+function runTest({query, results, extendedRangeDocs}) {
+ // Setup the collection and insert the dates in `standardDocs` and `extendedRangeDocs`.
const tsColl = db.getCollection(jsTestName());
tsColl.drop();
-
assert.commandWorked(
db.createCollection(tsColl.getName(), {timeseries: {timeField: timeFieldName}}));
-
- const dates = [
- // If underflow, we want to insert a date that would fall below the epoch
- // i.e. 1970-01-01 00:00:00 UTC. Otherwise we use a date within the epoch.
- {[timeFieldName]: underflow ? new Date("1965-01-01") : new Date("1971-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- {[timeFieldName]: new Date("1980-01-01")},
- {[timeFieldName]: new Date("1995-01-01")},
- // If overflow, we want to insert a date that would use more than 32 bit milliseconds after
- // the epoch. This overflow will occur 2038-01-29 03:13:07 UTC. Otherwise we go slightly
- // before the end of the 32 bit epoch.
- {[timeFieldName]: overflow ? new Date("2040-01-01") : new Date("2030-01-01")}
- ];
- assert.commandWorked(tsColl.insert(dates));
-
- // Make sure the expected results are in the correct order for comparison below.
- function cmpTimeFields(a, b) {
- return (b[timeFieldName].getTime() - a[timeFieldName].getTime());
- }
- results.sort(cmpTimeFields);
+ assert.commandWorked(tsColl.insert(standardDocs.concat(extendedRangeDocs)));
const pipeline = [{$match: query}, {$project: {_id: 0, [timeFieldName]: 1}}];
-
const plan = tsColl.explain().aggregate(pipeline);
- // Verify agg pipeline. We don't want to go through a plan that encourages a sort order to
- // avoid BUS and index selection, so we sort after gathering the results.
+ // Verify the results.
const aggActuals = tsColl.aggregate(pipeline).toArray();
- aggActuals.sort(cmpTimeFields);
- assert.docEq(results, aggActuals, JSON.stringify(plan, null, 4));
+ assert.sameMembers(results, aggActuals, JSON.stringify(plan, null, 4));
- // Verify the equivalent find command. We again don't want to go through a plan that
- // encourages a sort order to avoid BUS and index selection, so we sort after gathering the
- // results.
+ // Verify the equivalent find command.
let findActuals = tsColl.find(query, {_id: 0, [timeFieldName]: 1}).toArray();
- findActuals.sort(cmpTimeFields);
- assert.docEq(findActuals, results);
+ assert.sameMembers(results, findActuals, JSON.stringify(plan, null, 4));
}
-runTest(false,
- false,
- {[timeFieldName]: {$eq: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1980-01-01")}]);
-runTest(false,
- true,
- {[timeFieldName]: {$eq: new Date("2040-01-01")}},
- [{[timeFieldName]: new Date("2040-01-01")}]);
-runTest(true,
- false,
- {[timeFieldName]: {$eq: new Date("1965-01-01")}},
- [{[timeFieldName]: new Date("1965-01-01")}]);
-
-runTest(false,
- false,
- {[timeFieldName]: {$lt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1971-01-01")}, {[timeFieldName]: new Date("1975-01-01")}]);
-runTest(false,
- true,
- {[timeFieldName]: {$lt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1971-01-01")}, {[timeFieldName]: new Date("1975-01-01")}]);
-runTest(true,
- false,
- {[timeFieldName]: {$lt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1965-01-01")}, {[timeFieldName]: new Date("1975-01-01")}]);
-runTest(true,
- true,
- {[timeFieldName]: {$lt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1965-01-01")}, {[timeFieldName]: new Date("1975-01-01")}]);
-
-runTest(false,
- false,
- {[timeFieldName]: {$gt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1995-01-01")}, {[timeFieldName]: new Date("2030-01-01")}]);
-runTest(false,
- true,
- {[timeFieldName]: {$gt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1995-01-01")}, {[timeFieldName]: new Date("2040-01-01")}]);
-runTest(true,
- false,
- {[timeFieldName]: {$gt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1995-01-01")}, {[timeFieldName]: new Date("2030-01-01")}]);
-runTest(true,
- true,
- {[timeFieldName]: {$gt: new Date("1980-01-01")}},
- [{[timeFieldName]: new Date("1995-01-01")}, {[timeFieldName]: new Date("2040-01-01")}]);
-
-runTest(false, false, {[timeFieldName]: {$lte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1971-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- {[timeFieldName]: new Date("1980-01-01")}
-]);
-runTest(false, true, {[timeFieldName]: {$lte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1971-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- {[timeFieldName]: new Date("1980-01-01")}
-]);
-runTest(true, false, {[timeFieldName]: {$lte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1965-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- {[timeFieldName]: new Date("1980-01-01")}
-]);
-runTest(true, true, {[timeFieldName]: {$lte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1965-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- {[timeFieldName]: new Date("1980-01-01")}
-]);
-
-runTest(false, false, {[timeFieldName]: {$gte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1980-01-01")},
- {[timeFieldName]: new Date("1995-01-01")},
- {[timeFieldName]: new Date("2030-01-01")}
-]);
-runTest(false, true, {[timeFieldName]: {$gte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1980-01-01")},
- {[timeFieldName]: new Date("1995-01-01")},
- {[timeFieldName]: new Date("2040-01-01")}
-]);
-runTest(true, false, {[timeFieldName]: {$gte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1980-01-01")},
- {[timeFieldName]: new Date("1995-01-01")},
- {[timeFieldName]: new Date("2030-01-01")}
-]);
-runTest(true, true, {[timeFieldName]: {$gte: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1980-01-01")},
- {[timeFieldName]: new Date("1995-01-01")},
- {[timeFieldName]: new Date("2040-01-01")}
-]);
-
-// Verify ranges that straddle the lower and upper epoch boundaries work properly.
-runTest(
- true, false, {[timeFieldName]: {$gt: new Date("1920-01-01"), $lt: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1965-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- ]);
-runTest(
- false, true, {[timeFieldName]: {$gt: new Date("1980-01-01"), $lt: new Date("2050-01-01")}}, [
- {[timeFieldName]: new Date("1995-01-01")},
- {[timeFieldName]: new Date("2040-01-01")},
- ]);
+// Verify when there are no extended range dates in the collection, we handle querying by extended
+// range dates. The code for this is shared by multiple operators, so we don't have to test each
+// operator separately.
+runTest({
+ query: {[timeFieldName]: {$lte: beforeEpochDocs[1].time}},
+ results: [],
+ extendedRangeDocs: []
+});
+runTest({
+ query: {[timeFieldName]: {$lte: afterEpochDocs[0].time}},
+ results: standardDocs,
+ extendedRangeDocs: []
+});
+runTest({
+ query: {[timeFieldName]: {$gt: beforeEpochDocs[1].time}},
+ results: standardDocs,
+ extendedRangeDocs: []
+});
runTest(
- false, false, {[timeFieldName]: {$gt: new Date("1920-01-01"), $lt: new Date("1980-01-01")}}, [
- {[timeFieldName]: new Date("1971-01-01")},
- {[timeFieldName]: new Date("1975-01-01")},
- ]);
-runTest(
- false, false, {[timeFieldName]: {$gt: new Date("1980-01-01"), $lt: new Date("2050-01-01")}}, [
- {[timeFieldName]: new Date("1995-01-01")},
- {[timeFieldName]: new Date("2030-01-01")},
- ]);
-})();
+ {query: {[timeFieldName]: {$gt: afterEpochDocs[0].time}}, results: [], extendedRangeDocs: []});
+runTest({query: {[timeFieldName]: afterEpochDocs[0].time}, results: [], extendedRangeDocs: []});
+
+/*
+ * Verify $eq queries for dates inside, below and above the epoch ranges.
+ */
+// Test in the epoch range when there are measurements before and after the epoch.
+runTest({
+ query: {[timeFieldName]: {$eq: standardDocs[1].time}},
+ results: [standardDocs[1]],
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+// Test when there is one date above the epoch range. The date is one millisecond after the maximum
+// (the largest 32 bit integer) number of seconds since the epoch.
+runTest({
+ query: {[timeFieldName]: {$eq: afterEpochDocs[0].time}},
+ results: [afterEpochDocs[0]],
+ extendedRangeDocs: [afterEpochDocs[0]]
+});
+
+// Test when there are multiple dates after the epoch range.
+runTest({
+ query: {[timeFieldName]: {$eq: afterEpochDocs[0].time}},
+ results: [afterEpochDocs[0]],
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test when there is one date before the epoch. The date is one millisecond before the epoch.
+runTest({
+ query: {[timeFieldName]: {$eq: beforeEpochDocs[1].time}},
+ results: [beforeEpochDocs[1]],
+ extendedRangeDocs: [beforeEpochDocs[1]]
+});
+
+// Test when there are multiple dates before the epoch.
+runTest({
+ query: {[timeFieldName]: {$eq: beforeEpochDocs[0].time}},
+ results: [beforeEpochDocs[0]],
+ extendedRangeDocs: beforeEpochDocs
+});
+
+/*
+ * $lt queries.
+ */
+// Test with dates below the epoch that match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lt: beforeEpochDocs[1].time}},
+ results: [beforeEpochDocs[0]],
+ extendedRangeDocs: beforeEpochDocs
+});
+
+// Test with dates above the epoch that do not match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lt: standardDocs[2].time}},
+ results: [standardDocs[0], standardDocs[1]],
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test with dates both above and below the epoch. Only the before dates will match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lt: standardDocs[1].time}},
+ results: [standardDocs[0], beforeEpochDocs[0], beforeEpochDocs[1]],
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+// Test with dates both above and below the epoch. Both before and after dates match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lt: afterEpochDocs[1].time}},
+ results: standardDocs.concat(beforeEpochDocs, [afterEpochDocs[0]]),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+/*
+ * $gt queries.
+ */
+// Test with dates below the epoch that do not match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gt: beforeEpochDocs[1].time}},
+ results: standardDocs,
+ extendedRangeDocs: beforeEpochDocs
+});
+
+// Test with dates above the epoch that do not match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gt: afterEpochDocs[1].time}},
+ results: [],
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test with dates both above and below the epoch. Only the after dates will match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gt: standardDocs[2].time}},
+ results: [standardDocs[3], afterEpochDocs[0], afterEpochDocs[1]],
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+// Test with dates both above and below the epoch. Both before and after dates match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gt: beforeEpochDocs[0].time}},
+ results: standardDocs.concat([beforeEpochDocs[1]], afterEpochDocs),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+/*
+ * $lte queries.
+ */
+
+// Test with dates below the epoch that do match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lte: standardDocs[0].time}},
+ results: beforeEpochDocs.concat([standardDocs[0]]),
+ extendedRangeDocs: beforeEpochDocs
+});
+
+// Test with dates above the epoch that do match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lte: standardDocs[2].time}},
+ results: [standardDocs[0], standardDocs[1], standardDocs[2]],
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test with dates both above and below the epoch. Only the before dates will match the predicate.
+runTest({
+ query: {[timeFieldName]: {$lte: standardDocs[2].time}},
+ results: beforeEpochDocs.concat(standardDocs.slice(0, -1)),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+// Test with dates both above and below the epoch. Both before and after dates will match the
+// predicate.
+runTest({
+ query: {[timeFieldName]: {$lte: afterEpochDocs[0].time}},
+ results: standardDocs.concat(beforeEpochDocs, [afterEpochDocs[0]]),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+/*
+ * $gte queries.
+ */
+// Test with dates below the epoch that do not match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gte: standardDocs[1].time}},
+ results: standardDocs.slice(1),
+ extendedRangeDocs: beforeEpochDocs
+});
+
+// Test with dates above the epoch that do match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gte: standardDocs[2].time}},
+ results: afterEpochDocs.concat(standardDocs.slice(2)),
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test with dates both above and below the epoch. Only the after dates will match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gte: standardDocs[1].time}},
+ results: afterEpochDocs.concat(standardDocs.slice(1)),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+// Test with dates both above and below the epoch. Both before and after dates will match the
+// predicate.
+runTest({
+ query: {[timeFieldName]: {$gte: beforeEpochDocs[1].time}},
+ results: afterEpochDocs.concat(standardDocs, [beforeEpochDocs[1]]),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+/*
+ * Compound predicates ($and, $or).
+ */
+
+// Test with dates below the epoch that do match the predicate.
+runTest({
+ query: {[timeFieldName]: {$gt: ISODate("1920-01-01"), $lt: ISODate("1980-01-01")}},
+ results: beforeEpochDocs.concat([standardDocs[0]]),
+ extendedRangeDocs: beforeEpochDocs
+});
+
+// Test with dates below the epoch that do not match the predicate.
+runTest({
+ query:
+ {[timeFieldName]: {$gt: ISODate("1970-01-01T00:00:00.001Z"), $lt: ISODate("1980-01-01")}},
+ results: [standardDocs[0]],
+ extendedRangeDocs: beforeEpochDocs
+});
+
+// Test with dates above the epoch that do match the predicate.
+runTest({
+ query: {
+ [timeFieldName]:
+ {$gt: ISODate("2030-09-29T23:59:59.001Z"), $lt: ISODate("2050-01-20T03:14:00.001Z")}
+ },
+ results: [standardDocs[3], afterEpochDocs[0]],
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test with dates above the epoch that do not match the predicate.
+runTest({
+ query: {
+ [timeFieldName]:
+ {$gt: ISODate("2030-09-29T23:59:59.001Z"), $lt: ISODate("2038-01-19T03:14:00.000Z")}
+ },
+ results: [standardDocs[3]],
+ extendedRangeDocs: afterEpochDocs
+});
+
+// Test with dates both above and below the epoch. Both before and after dates will match the
+// predicate.
+runTest({
+ query: {
+ $or: [
+ {[timeFieldName]: {$lt: ISODate("1975-12-01T23:59:00.001Z")}},
+ {[timeFieldName]: {$gt: ISODate("2038-01-19T03:14:07.002Z")}}
+ ]
+ },
+ results: beforeEpochDocs.concat([standardDocs[0], afterEpochDocs[1]]),
+ extendedRangeDocs: allExtendedRangeDocs
+});
+
+// Test with dates both above and below the epoch. Neither before nor after dates will match the
+// predicate.
+runTest({
+ query: {[timeFieldName]: {$in: [standardDocs[1].time, standardDocs[3].time]}},
+ results: [standardDocs[1], standardDocs[3]],
+ extendedRangeDocs: allExtendedRangeDocs
+});
+})(); \ No newline at end of file
diff --git a/jstests/core/timeseries/timeseries_geonear_lookup.js b/jstests/core/timeseries/timeseries_geonear_lookup.js
new file mode 100644
index 00000000000..78f68eb014e
--- /dev/null
+++ b/jstests/core/timeseries/timeseries_geonear_lookup.js
@@ -0,0 +1,40 @@
+/**
+ * Test that time-series works as expected with $geoNear within a $lookup.
+ *
+ * @tags: [
+ * does_not_support_transactions,
+ * requires_pipeline_optimization,
+ * # We need a timeseries collection.
+ * requires_timeseries,
+ * references_foreign_collection,
+ * ]
+ */
+
+// create a timeseries collection
+const timeFieldName = "time";
+const metaFieldName = "tags";
+const testDB = db;
+
+const tsColl = db.getCollection("ts_coll");
+
+const kMaxDistance = Math.PI * 2.0;
+
+tsColl.drop();
+assert.commandWorked(testDB.createCollection(
+ tsColl.getName(), {timeseries: {timeField: timeFieldName, metaField: metaFieldName}}));
+
+assert.commandWorked(tsColl.createIndex({'tags.loc': '2dsphere'}));
+
+tsColl.insert({time: ISODate(), tags: {loc: [40, 40], descr: 0}, value: 0});
+
+const coll2 = db.getCollection("store_min_max_values");
+coll2.drop();
+assert.commandWorked(coll2.insert({_id: 0, minimumDist: 0.0, maximumDist: kMaxDistance}));
+
+coll2.aggregate([{$lookup: {from: tsColl.getName(),
+let: {minVal: "$minimumDist",maxVal:"$maximumDist"},
+pipeline: [
+ {$geoNear: {near: {type: "Point", coordinates: [0, 0]},
+ key: 'tags.loc',
+ distanceField: "tags.distance"}}],
+ as: 'output'}}]);
diff --git a/jstests/core/timeseries/timeseries_graph_lookup.js b/jstests/core/timeseries/timeseries_graph_lookup.js
index 7b152cc6b1f..25f47aa2cef 100644
--- a/jstests/core/timeseries/timeseries_graph_lookup.js
+++ b/jstests/core/timeseries/timeseries_graph_lookup.js
@@ -5,6 +5,7 @@
* does_not_support_transactions,
* requires_timeseries,
* requires_fcv_51,
+ * references_foreign_collection,
* ]
*/
(function() {
diff --git a/jstests/core/timeseries/timeseries_insert_mixed_schema_bucket.js b/jstests/core/timeseries/timeseries_insert_mixed_schema_bucket.js
new file mode 100644
index 00000000000..cbce31e9795
--- /dev/null
+++ b/jstests/core/timeseries/timeseries_insert_mixed_schema_bucket.js
@@ -0,0 +1,75 @@
+/**
+ * Tests directly inserting a time-series bucket with mixed schema.
+ *
+ * @tags: [
+ * # $listCatalog does not include the tenant prefix in its results.
+ * command_not_supported_in_serverless,
+ * requires_timeseries,
+ * # $listCatalog not supported inside of a multi-document transaction
+ * does_not_support_transactions,
+ * # $listCatalog only exists since v6
+ * multiversion_incompatible,
+ * ]
+ */
+(function() {
+"use strict";
+
+load("jstests/core/timeseries/libs/timeseries.js"); // For 'TimeseriesTest'.
+
+TestData.skipEnforceTimeseriesBucketsAreAlwaysCompressedOnValidate = true;
+
+const testDB = db.getSiblingDB(jsTestName());
+const collName = "ts";
+
+assert.commandWorkedOrFailedWithCode(testDB.runCommand({drop: collName}),
+ ErrorCodes.NamespaceNotFound);
+assert.commandWorked(
+ testDB.createCollection(collName, {timeseries: {timeField: "t", metaField: "m"}}));
+const coll = testDB[collName];
+const bucketsColl = testDB["system.buckets." + collName];
+
+const bucket = {
+ _id: ObjectId("65a6eb806ffc9fa4280ecac4"),
+ control: {
+ version: NumberInt(1),
+ min: {
+ _id: ObjectId("65a6eba7e6d2e848e08c3750"),
+ t: ISODate("2024-01-16T20:48:00Z"),
+ a: 1,
+ },
+ max: {
+ _id: ObjectId("65a6eba7e6d2e848e08c3751"),
+ t: ISODate("2024-01-16T20:48:39.448Z"),
+ a: "a",
+ },
+ },
+ meta: 0,
+ data: {
+ _id: {
+ 0: ObjectId("65a6eba7e6d2e848e08c3750"),
+ 1: ObjectId("65a6eba7e6d2e848e08c3751"),
+ },
+ t: {
+ 0: ISODate("2024-01-16T20:48:39.448Z"),
+ 1: ISODate("2024-01-16T20:48:39.448Z"),
+ },
+ a: {
+ 0: "a",
+ 1: 1,
+ },
+ }
+};
+
+assert.commandFailedWithCode(bucketsColl.insert(bucket),
+ ErrorCodes.CannotInsertTimeseriesBucketsWithMixedSchema);
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), false);
+assert.commandWorked(
+ testDB.runCommand({collMod: collName, timeseriesBucketsMayHaveMixedSchemaData: true}));
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), true);
+assert.commandWorked(bucketsColl.insert(bucket));
+assert.commandWorked(bucketsColl.deleteOne({_id: bucket._id}));
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), true);
+assert.commandWorked(
+ testDB.runCommand({collMod: collName, timeseriesBucketsMayHaveMixedSchemaData: false}));
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), false);
+})();
diff --git a/jstests/core/timeseries/timeseries_lookup.js b/jstests/core/timeseries/timeseries_lookup.js
index ba804c4ec92..6d559e9356d 100644
--- a/jstests/core/timeseries/timeseries_lookup.js
+++ b/jstests/core/timeseries/timeseries_lookup.js
@@ -5,11 +5,13 @@
* does_not_support_transactions,
* requires_timeseries,
* requires_fcv_51,
+ * references_foreign_collection,
* ]
*/
(function() {
"use strict";
+load("jstests/aggregation/extras/utils.js");
load("jstests/core/timeseries/libs/timeseries.js");
TimeseriesTest.run((insert) => {
@@ -245,3 +247,91 @@ TimeseriesTest.run((insert) => {
});
});
})();
+
+{
+ // Test that we get the right results for $lookup on a timeseries collection with an internal
+ // pipeline containing both correlated and uncorrelated $match stages. Ensures we do not cache
+ // the results of this internal pipeline incorrectly
+ // (src/mongo/db/pipeline/document_source_sequential_document_cache.h) regardless of the order
+ // of the $match stages.
+ const testDB = db.getSiblingDB(jsTestName());
+ testDB.local.insertMany([{_id: 0, key: 1}, {_id: 1, key: 2}]);
+ testDB.createCollection("foreign", {timeseries: {timeField: "time", metaField: "meta"}});
+ testDB.foreign.insertMany([
+ {time: new Date(), _id: 0, meta: 1, val1: 42, val2: 100},
+ {time: new Date(), _id: 2, meta: 2, val1: 17, val2: 100}
+ ]);
+
+ // The $sequentialCache (document_source_sequential_document_cache.h) decides whether or not it
+ // can cache the results of the internal lookup pipeline based on if the pipeline is correlated
+ // or uncorrelated. With timeseries, many rewrites occur which can merge $match stages together,
+ // have them pushed into the $_internalUnpackBucket stage as an eventFilter, or push them in
+ // front of the
+ // $_internalUnpackBucket stage. We need to make sure that the ability of the cache to recognize
+ // when there is a correlated $match in the pipeline to remain regardless of these rewrites.
+ (function testUncorrelatedFollowedByCorrelatedMatch() {
+ const lookupStage = {$lookup: {
+ from: "foreign",
+ let: {lkey: "$key"},
+ pipeline: [
+ {$match: {$expr: {$lt: ["$val1","$val2"]}}},
+ {$match: {$expr: {$eq: ["$meta","$$lkey"]}}},
+ {$project: {lkey: "$$lkey",fkey: "$meta",val: "$val1",_id: 0,}},
+ ],
+ as: "joined"
+ }};
+ const result = testDB.local.aggregate(lookupStage);
+ assertArrayEq({
+ actual: result.toArray(),
+ expected: [
+ {"_id": 1, "key": 2, "joined": [{"lkey": 2, "fkey": 2, "val": 17}]},
+ {"_id": 0, "key": 1, "joined": [{"lkey": 1, "fkey": 1, "val": 42}]}
+ ],
+ extraErrorMsg: `Unexpected result of running pipeline ${tojson(lookupStage)}`
+ });
+ })();
+
+ (function testCorrelatedFollowedByUncorrelatedMatch() {
+ const lookupStage = {$lookup: {
+ from: "foreign",
+ let: {lkey: "$key"},
+ pipeline: [
+ {$match: {$expr: {$eq: ["$meta","$$lkey"]}}},
+ {$match: {$expr: {$lt: ["$val1","$val2"]}}},
+ {$project: {lkey: "$$lkey",fkey: "$meta",val: "$val1",_id: 0,}},
+ ],
+ as: "joined"
+ }};
+ const result = testDB.local.aggregate(lookupStage);
+ assertArrayEq({
+ actual: result.toArray(),
+ expected: [
+ {"_id": 1, "key": 2, "joined": [{"lkey": 2, "fkey": 2, "val": 17}]},
+ {"_id": 0, "key": 1, "joined": [{"lkey": 1, "fkey": 1, "val": 42}]}
+ ],
+ extraErrorMsg: `Unexpected result of running pipeline ${tojson(lookupStage)}`
+ });
+ })();
+
+ (function testUncorrelatedFollowedByUncorrelatedMatch() {
+ const lookupStage = {$lookup: {
+ from: "foreign",
+ let: {lkey: "$key"},
+ pipeline: [
+ {$match: {$expr: {$lt: ["$meta","$val1"]}}},
+ {$match: {$expr: {$lt: ["$val1","$val2"]}}},
+ {$project: {meta: "$meta",val: "$val1",_id: 0,}},
+ ],
+ as: "joined"
+ }};
+ const result = testDB.local.aggregate(lookupStage);
+ assertArrayEq({
+ actual: result.toArray(),
+ expected: [
+ {"_id": 0, "key": 1, "joined": [{"meta": 1, "val": 42}, {"meta": 2, "val": 17}]},
+ {"_id": 1, "key": 2, "joined": [{"meta": 1, "val": 42}, {"meta": 2, "val": 17}]}
+ ],
+ extraErrorMsg: `Unexpected result of running pipeline ${tojson(lookupStage)}`
+ });
+ })();
+}
diff --git a/jstests/core/timeseries/timeseries_merge.js b/jstests/core/timeseries/timeseries_merge.js
index 59d4dbee889..fb6a23df803 100644
--- a/jstests/core/timeseries/timeseries_merge.js
+++ b/jstests/core/timeseries/timeseries_merge.js
@@ -6,6 +6,7 @@
* does_not_support_stepdowns,
* does_not_support_transactions,
* requires_timeseries,
+ * references_foreign_collection,
* ]
*/
(function() {
diff --git a/jstests/core/timeseries/timeseries_project.js b/jstests/core/timeseries/timeseries_project.js
index 11ca90f2efe..b735a8a3b66 100644
--- a/jstests/core/timeseries/timeseries_project.js
+++ b/jstests/core/timeseries/timeseries_project.js
@@ -129,4 +129,171 @@ tsDoc = tsColl.aggregate(pipeline).toArray();
regDoc = regColl.aggregate(pipeline).toArray();
assert.docEq(tsDoc, regDoc);
})();
+
+// Test that an includes projection that doesn't include previously included computed meta fields
+// outputs correctly.
+(function testIncludeComputedMetaFieldThenProjectWithoutComputedMetaField() {
+ coll.drop();
+ assert.commandWorked(
+ db.createCollection(coll.getName(), {timeseries: {timeField: 'time', metaField: 'tag'}}));
+
+ const doc = {_id: 0, time: ISODate("2024-01-01T00:00:00.000Z"), tag: {field: "A"}};
+ assert.commandWorked(coll.insert(doc));
+ let result = {};
+
+ // Added field which uses meta should not be in the result.
+ result =
+ coll.aggregate(
+ [{$addFields: {time: {$dateFromParts: {year: "$tag.none"}}}}, {$project: {tag: 1}}])
+ .toArray();
+ assert.docEq([{"tag": {"field": "A"}, "_id": 0}], result);
+
+ // Test with non-null value.
+ result = coll.aggregate([{$addFields: {time: {$toLower: "$tag.field"}}}, {$project: {tag: 1}}])
+ .toArray();
+ assert.docEq([{"tag": {"field": "A"}, "_id": 0}], result);
+
+ // Testing with $match between $addFields and $project.
+ result = coll.aggregate([
+ {$addFields: {time: {$dateFromParts: {year: "$tag.none"}}}},
+ {$match: {tag: {field: "A"}}},
+ {$project: {tag: 1}}
+ ])
+ .toArray();
+ assert.docEq([{"tag": {field: "A"}, "_id": 0}], result);
+
+ // Test with non-null value.
+ result =
+ coll.aggregate(
+ [{$addFields: {time: "$tag.field"}}, {$match: {time: "A"}}, {$project: {tag: 1}}])
+ .toArray();
+ assert.docEq([{"tag": {field: "A"}, "_id": 0}], result);
+
+ // Testing with $project after $project.
+ result = coll.aggregate([{$project: {tag: 1}}, {$project: {time: 1}}]).toArray();
+ assert.docEq([{"_id": 0}], result);
+
+ // Testing with $set.
+ result = coll.aggregate([{$set: {tag: "$tag.field"}}, {$project: {time: 1}}]).toArray();
+ assert.docEq([{"_id": 0, "time": ISODate("2024-01-01T00:00:00Z")}], result);
+
+ // Test that computedMetaFields that are included by the $project are still included.
+ result = coll.aggregate([
+ {$addFields: {hi: {$dateFromParts: {year: "$tag.none"}}}},
+ {$addFields: {bye: {$dateFromParts: {year: "$tag.none"}}}},
+ {$project: {hi: 1}}
+ ])
+ .toArray();
+ assert.docEq([{"_id": 0, "hi": null}], result);
+
+ // Test with non-null value.
+ result = coll.aggregate([
+ {$addFields: {hi: "$tag.field"}},
+ {$addFields: {bye: "$tag.field"}},
+ {$project: {hi: 1}}
+ ])
+ .toArray();
+ assert.docEq([{"_id": 0, "hi": "A"}], result);
+
+ // Test that $project that replaces field that is used works.
+ result = coll.aggregate([
+ {$addFields: {hello: "$tag.field"}},
+ {$project: {newTime: "$time", time: "$tag.field"}}
+ ])
+ .toArray();
+
+ assert.docEq([{"_id": 0, newTime: ISODate("2024-01-01T00:00:00.000Z"), time: "A"}], result);
+
+ // Test adding fields and including one of them.
+ result = coll.aggregate([
+ {$addFields: {a: {$toLower: "$tag.field"}}},
+ {$addFields: {b: {$toUpper: "$tag.field"}}},
+ {$project: {b: 1}}
+ ])
+ .toArray();
+ assert.docEq([{b: "A", "_id": 0}], result);
+})();
+
+// Test that an excludes projection that excludes previously included computed meta fields
+// outputs correctly.
+(function testIncludeComputedMetaFieldThenExcludeComputedMetaField() {
+ coll.drop();
+ assert.commandWorked(
+ db.createCollection(coll.getName(), {timeseries: {timeField: 'time', metaField: 'tag'}}));
+
+ const doc = {_id: 0, time: ISODate("2024-01-01T00:00:00.000Z"), tag: {field: "A"}};
+ assert.commandWorked(coll.insert(doc));
+ let result = {};
+
+ // Excluded '_computedMetaProjField' should not be included.
+ result = coll.aggregate([
+ {$addFields: {hello: {$dateFromParts: {year: "$tag.none"}}}},
+ {$project: {hello: 0}}
+ ])
+ .toArray();
+ assert.docEq([{"time": ISODate("2024-01-01T00:00:00Z"), "tag": {"field": "A"}, "_id": 0}],
+ result);
+
+ // Test with non-null value.
+ result = coll.aggregate([{$addFields: {time: {$toLower: "$tag.field"}}}, {$project: {time: 0}}])
+ .toArray();
+ assert.docEq([{"tag": {"field": "A"}, "_id": 0}], result);
+
+ // Testing with $match between $addFields and $project.
+ result = coll.aggregate([
+ {$addFields: {time: {$dateFromParts: {year: "$tag.none"}}}},
+ {$match: {tag: {field: "A"}}},
+ {$project: {time: 0}}
+ ])
+ .toArray();
+ assert.docEq([{"tag": {field: "A"}, "_id": 0}], result);
+
+ // Test with non-null value.
+ result =
+ coll.aggregate(
+ [{$addFields: {time: "$tag.field"}}, {$match: {time: "A"}}, {$project: {time: 0}}])
+ .toArray();
+ assert.docEq([{"tag": {field: "A"}, "_id": 0}], result);
+
+ // Testing with exclude $project after include $project.
+ result = coll.aggregate([{$project: {tag: 1}}, {$project: {tag: 0}}]).toArray();
+ assert.docEq([{"_id": 0}], result);
+
+ // Testing with $set.
+ result = coll.aggregate([{$set: {tag: "$tag.field"}}, {$project: {tag: 0}}]).toArray();
+ assert.docEq([{"time": ISODate("2024-01-01T00:00:00Z"), "_id": 0}], result);
+
+ // Exclude one added field.
+ result = coll.aggregate([
+ {$addFields: {hi: {$dateFromParts: {year: "$tag.none"}}}},
+ {$addFields: {bye: {$dateFromParts: {year: "$tag.none"}}}},
+ {$project: {hi: 0}}
+ ])
+ .toArray();
+ assert.docEq(
+ [{"time": ISODate("2024-01-01T00:00:00Z"), "tag": {"field": "A"}, "_id": 0, "bye": null}],
+ result);
+
+ // Test with non-null value.
+ result = coll.aggregate([
+ {$addFields: {hi: "$tag.field"}},
+ {$addFields: {bye: "$tag.field"}},
+ {$project: {hi: 0}}
+ ])
+ .toArray();
+ assert.docEq(
+ [{"time": ISODate("2024-01-01T00:00:00Z"), "tag": {"field": "A"}, "_id": 0, "bye": "A"}],
+ result);
+
+ // Test adding fields and excluding one of them.
+ result = coll.aggregate([
+ {$addFields: {a: {$toLower: "$tag.field"}}},
+ {$addFields: {b: {$toUpper: "$tag.field"}}},
+ {$project: {b: 0}}
+ ])
+ .toArray();
+ assert.docEq(
+ [{"time": ISODate("2024-01-01T00:00:00Z"), "tag": {"field": "A"}, "_id": 0, "a": "a"}],
+ result);
+})();
})();
diff --git a/jstests/core/timeseries/timeseries_union_with.js b/jstests/core/timeseries/timeseries_union_with.js
index b5d5369dcc8..e92e4c4ca37 100644
--- a/jstests/core/timeseries/timeseries_union_with.js
+++ b/jstests/core/timeseries/timeseries_union_with.js
@@ -8,6 +8,7 @@
* # migrations may result in writes splitting between two primaries, and thus different buckets.
* does_not_support_stepdowns,
* tenant_migration_incompatible,
+ * references_foreign_collection,
* ]
*/
(function() {
diff --git a/jstests/core/timeseries/timeseries_update_mixed_schema_bucket.js b/jstests/core/timeseries/timeseries_update_mixed_schema_bucket.js
new file mode 100644
index 00000000000..0211daa29d8
--- /dev/null
+++ b/jstests/core/timeseries/timeseries_update_mixed_schema_bucket.js
@@ -0,0 +1,79 @@
+/**
+ * Tests directly updating a time-series bucket to contain mixed schema.
+ *
+ * @tags: [
+ * # $listCatalog does not include the tenant prefix in its results.
+ * command_not_supported_in_serverless,
+ * requires_timeseries,
+ * # $listCatalog not supported inside of a multi-document transaction
+ * does_not_support_transactions,
+ * # $listCatalog only exists since v6
+ * multiversion_incompatible,
+ * ]
+ */
+(function() {
+"use strict";
+load("jstests/core/timeseries/libs/timeseries.js"); // For 'TimeseriesTest'.
+
+TestData.skipEnforceTimeseriesBucketsAreAlwaysCompressedOnValidate = true;
+
+const testDB = db.getSiblingDB(jsTestName());
+const collName = "ts";
+
+assert.commandWorkedOrFailedWithCode(testDB.runCommand({drop: collName}),
+ ErrorCodes.NamespaceNotFound);
+assert.commandWorked(
+ testDB.createCollection(collName, {timeseries: {timeField: "t", metaField: "m"}}));
+const coll = testDB[collName];
+const bucketsColl = testDB["system.buckets." + collName];
+
+const bucket = {
+ _id: ObjectId("65a6eb806ffc9fa4280ecac4"),
+ control: {
+ version: NumberInt(1),
+ min: {
+ _id: ObjectId("65a6eba7e6d2e848e08c3750"),
+ t: ISODate("2024-01-16T20:48:00Z"),
+ a: 0,
+ },
+ max: {
+ _id: ObjectId("65a6eba7e6d2e848e08c3751"),
+ t: ISODate("2024-01-16T20:48:39.448Z"),
+ a: 1,
+ },
+ },
+ meta: 0,
+ data: {
+ _id: {
+ 0: ObjectId("65a6eba7e6d2e848e08c3750"),
+ 1: ObjectId("65a6eba7e6d2e848e08c3751"),
+ },
+ t: {
+ 0: ISODate("2024-01-16T20:48:39.448Z"),
+ 1: ISODate("2024-01-16T20:48:39.448Z"),
+ },
+ a: {
+ 0: 0,
+ 1: 1,
+ },
+ }
+};
+
+const update = function() {
+ return bucketsColl.update({_id: bucket._id},
+ {$set: {"control.min.a": 1, "control.max.a": "a", "data.a.0": "a"}});
+};
+
+assert.commandWorked(bucketsColl.insert(bucket));
+assert.commandFailedWithCode(update(), ErrorCodes.CannotInsertTimeseriesBucketsWithMixedSchema);
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), false);
+assert.commandWorked(
+ testDB.runCommand({collMod: collName, timeseriesBucketsMayHaveMixedSchemaData: true}));
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), true);
+assert.commandWorked(update());
+assert.commandWorked(bucketsColl.deleteOne({_id: bucket._id}));
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), true);
+assert.commandWorked(
+ testDB.runCommand({collMod: collName, timeseriesBucketsMayHaveMixedSchemaData: false}));
+assert.eq(TimeseriesTest.bucketsMayHaveMixedSchemaData(bucketsColl), false);
+})();
diff --git a/jstests/core/txns/abort_expired_transaction.js b/jstests/core/txns/abort_expired_transaction.js
index b211ef7b132..54418596967 100644
--- a/jstests/core/txns/abort_expired_transaction.js
+++ b/jstests/core/txns/abort_expired_transaction.js
@@ -33,6 +33,11 @@ try {
const session = db.getMongo().startSession(sessionOptions);
const sessionDb = session.getDatabase(testDBName);
+ // Number of passes made by the "abortExpiredTransactions" thread before the transaction
+ // expires.
+ const abortExpiredTransactionsPassesPreAbort =
+ db.serverStatus().metrics.abortExpiredTransactions.passes;
+
let txnNumber = 0;
jsTest.log("Insert a document starting a transaction.");
@@ -64,6 +69,14 @@ try {
"currentOp reports that the idle transaction still exists, it has not been " +
"aborted as expected.");
+ assert.soon(() => {
+ // For this expired transaction to abort, the "abortExpiredTransactions" thread has to
+ // perform at least one pass.
+ const serverStatus = db.serverStatus();
+ return abortExpiredTransactionsPassesPreAbort <
+ serverStatus.metrics.abortExpiredTransactions.passes;
+ });
+
jsTest.log(
"Attempt to do a write in the transaction, which should fail because the transaction " +
"was aborted");
diff --git a/jstests/core/txns/aggregation_in_transaction.js b/jstests/core/txns/aggregation_in_transaction.js
index ec1c62133bb..40d7030fe1f 100644
--- a/jstests/core/txns/aggregation_in_transaction.js
+++ b/jstests/core/txns/aggregation_in_transaction.js
@@ -1,5 +1,5 @@
// Tests that aggregation is supported in transactions.
-// @tags: [uses_transactions, uses_snapshot_read_concern]
+// @tags: [uses_transactions, uses_snapshot_read_concern, references_foreign_collection]
(function() {
"use strict";
diff --git a/jstests/core/txns/prepare_conflict_aggregation_behavior.js b/jstests/core/txns/prepare_conflict_aggregation_behavior.js
index c62b7370dc6..c07f96e2b54 100644
--- a/jstests/core/txns/prepare_conflict_aggregation_behavior.js
+++ b/jstests/core/txns/prepare_conflict_aggregation_behavior.js
@@ -3,7 +3,12 @@
* should not block on prepare conflicts, but writing out to a collection as a part of an aggregate
* pipeline should block on prepare conflicts.
*
- * @tags: [uses_transactions, uses_prepare_transaction]
+ * The test runs commands that are not allowed with security token: endSession, prepareTransaction.
+ * @tags: [
+ * references_foreign_collection,
+ * uses_transactions,
+ * uses_prepare_transaction,
+ * ]
*/
(function() {
"use strict";
diff --git a/jstests/core/views/invalid_system_views.js b/jstests/core/views/invalid_system_views.js
index da2b60d170f..15b4ab6bc3b 100644
--- a/jstests/core/views/invalid_system_views.js
+++ b/jstests/core/views/invalid_system_views.js
@@ -14,6 +14,7 @@
* # The drop of offending views may not happen on the donor after a committed migration.
* tenant_migration_incompatible,
* uses_compact,
+ * references_foreign_collection,
* ]
*/
diff --git a/jstests/core/views/views_aggregation.js b/jstests/core/views/views_aggregation.js
index 537ec3ee008..287bc5af667 100644
--- a/jstests/core/views/views_aggregation.js
+++ b/jstests/core/views/views_aggregation.js
@@ -9,6 +9,7 @@
* requires_non_retryable_commands,
* # Explain of a resolved view must be executed by mongos.
* directly_against_shardsvrs_incompatible,
+ * references_foreign_collection,
* ]
*/
(function() {
diff --git a/jstests/core/views/views_collation.js b/jstests/core/views/views_collation.js
index 169e9309740..65deb11edbc 100644
--- a/jstests/core/views/views_collation.js
+++ b/jstests/core/views/views_collation.js
@@ -6,6 +6,7 @@
// requires_non_retryable_commands,
// # Explain of a resolved view must be executed by mongos.
// directly_against_shardsvrs_incompatible,
+// references_foreign_collection,
// ]
/**
diff --git a/jstests/core/views/views_creation.js b/jstests/core/views/views_creation.js
index 9528199996a..c24a008342d 100644
--- a/jstests/core/views/views_creation.js
+++ b/jstests/core/views/views_creation.js
@@ -11,6 +11,7 @@
* requires_non_retryable_commands,
* # Tenant migrations don't support applyOps.
* tenant_migration_incompatible,
+ * references_foreign_collection,
* ]
*/
(function() {
diff --git a/jstests/core/views/views_validation.js b/jstests/core/views/views_validation.js
index 02c060fd50a..4dea7c9491b 100644
--- a/jstests/core/views/views_validation.js
+++ b/jstests/core/views/views_validation.js
@@ -2,6 +2,7 @@
// # Running getCollection on views in sharded suites tries to shard views, which fails.
// assumes_unsharded_collection,
// requires_non_retryable_commands,
+// references_foreign_collection,
// ]
(function() {
diff --git a/jstests/core/wildcard_index_validindex.js b/jstests/core/wildcard_index_validindex.js
index 24ecdcf77cf..c4a2ea9c2ac 100644
--- a/jstests/core/wildcard_index_validindex.js
+++ b/jstests/core/wildcard_index_validindex.js
@@ -98,6 +98,18 @@ assert.commandFailedWithCode(coll.createIndex({"$**": "wildcard"}), ErrorCodes.C
// Cannot create a compound wildcard index.
assert.commandFailedWithCode(coll.createIndex({"$**": 1, "a": 1}), ErrorCodes.CannotCreateIndex);
assert.commandFailedWithCode(coll.createIndex({"a": 1, "$**": 1}), ErrorCodes.CannotCreateIndex);
+assert.commandFailedWithCode(coll.createIndex({"obj.$**": 1, "a": 1}),
+ ErrorCodes.CannotCreateIndex);
+assert.commandFailedWithCode(db.runCommand({
+ createIndexes: kCollectionName,
+ indexes: [{wildcardProjection: {a: 1, c: 1, d: 1}, key: {"$**": 1, b: 1}}]
+}),
+ ErrorCodes.CannotCreateIndex);
+assert.commandFailedWithCode(db.runCommand({
+ createIndexes: kCollectionName,
+ indexes: [{key: {"$**": 1, b: 1}, wildcardProjection: {a: 1, c: 1, d: 1}}]
+}),
+ ErrorCodes.CannotCreateIndex);
// Cannot create an wildcard index with an invalid spec.
assert.commandFailedWithCode(coll.createIndex({"a.$**.$**": 1}), ErrorCodes.CannotCreateIndex);
@@ -141,4 +153,9 @@ assert.commandFailedWithCode(
assert.commandFailedWithCode(
createIndexHelper({"a.$**": 1}, {name: kIndexName, wildcardProjection: {b: 0}}),
ErrorCodes.FailedToParse);
+assert.commandFailedWithCode(db.runCommand({
+ createIndexes: kCollectionName,
+ indexes: [{wildcardProjection: {b: 1}, key: {"a.$**": 1}}]
+}),
+ ErrorCodes.FailedToParse);
})();