Files
mongo/jstests/core/query/field_name_validation.js
Shin Yee Tan 888bac8958 SERVER-120044 Disable testing retryable findAndModify in ASC with PDIB enabled (#48396)
GitOrigin-RevId: 2f8d46302ab33d084528547881ca88cd54ccc07c
2026-02-23 18:27:52 +00:00

302 lines
12 KiB
JavaScript

/**
* Test the behavior of field names containing special characters, including:
* - dots
* - $-prefixed
* - reserved $-prefixed ($ref, $id, $db)
* - regex
*
* contained in a top-level element, embedded element, and within _id.
*
* @tags: [
* assumes_unsharded_collection,
* requires_getmore,
* # Time-series collections have different _id properties.
* exclude_from_timeseries_crud_passthrough,
* # Primary-driven index builds must have batched writes enabled which config.image_collection
* # does not support.
* primary_driven_index_builds_incompatible_with_retryable_writes,
* ]
*/
const coll = db.field_name_validation;
coll.drop();
//
// Insert command field name validation.
//
// Test that dotted field names are allowed.
assert.commandWorked(coll.insert({"a.b": 1}));
assert.commandWorked(coll.insert({"_id.a": 1}));
assert.commandWorked(coll.insert({a: {"a.b": 1}}));
assert.commandWorked(coll.insert({_id: {"a.b": 1}}));
// Test that _id cannot be a regex.
assert.writeError(coll.insert({_id: /a/}));
// Test that _id cannot be an array.
assert.writeError(coll.insert({_id: [9]}));
// Test that $-prefixed field names are allowed in embedded objects.
assert.commandWorked(coll.insert({a: {$b: 1}}));
assert.eq(1, coll.find({"a.$b": 1}).itcount());
// An embedded _id field can be an object with an element that has a $-prefixed name.
assert.commandWorked(coll.insert({a: {_id: [9]}}));
assert.commandWorked(coll.insert({a: {_id: /a/}}));
assert.commandWorked(coll.insert({a: {_id: {$b: 1}}}));
// Test that $-prefixed field names are allowed generally.
assert.commandWorked(coll.insert({$a: 1}));
assert.commandWorked(coll.insert({valid: 1, $a: 1}));
assert.commandWorked(coll.insert({$a: {$b: 1}}));
// Test that reserved $-prefixed field names are allowed.
assert.commandWorked(coll.insert({$ref: 1}));
assert.commandWorked(coll.insert({$id: 1}));
assert.commandWorked(coll.insert({$db: 1}));
// Test that _id cannot be an object with an element that has a $-prefixed field name.
assert.writeErrorWithCode(coll.insert({_id: {$b: 1}}), ErrorCodes.DollarPrefixedFieldName);
assert.writeErrorWithCode(coll.insert({_id: {a: 1, $b: 1}}), ErrorCodes.DollarPrefixedFieldName);
// Test that inserting an object with a $-prefixed field name is properly validated.
assert.commandWorked(coll.insert({_id: 0, $valid: 1, "a": 1}));
assert.eq([{_id: 0, $valid: 1, "a": 1}], coll.find({_id: 0}).toArray());
// Test that previously reserved fieldnames may be inserted when the feature flag is enabled.
assert.commandWorked(coll.insert({_id: 1, $valid: 1, $id: 1}));
assert.commandWorked(coll.insert({_id: 2, $valid: 1, $db: 1}));
assert.commandWorked(coll.insert({_id: 3, $valid: 1, $ref: 1}));
assert.commandWorked(coll.insert({_id: 4, $valid: 1, $alsoValid: 1}));
// Valid, because _id.$gt is a field name, and not equivalent to {_id: {$gt: 4}}
assert.commandWorked(coll.insert({"_id.$gt": 4}));
//
// Update command field name validation.
//
coll.drop();
// Dotted fields are allowed in an update.
assert.commandWorked(coll.update({}, {"a.b": 1}, {upsert: true}));
assert.eq(0, coll.find({"a.b": 1}).itcount());
assert.eq(1, coll.find({}).itcount());
// Dotted fields represent paths in $set.
assert.commandWorked(coll.update({}, {$set: {"a.b": 1}}, {upsert: true}));
assert.eq(1, coll.find({"a.b": 1}).itcount());
// Dotted fields represent paths in the query object.
assert.commandWorked(coll.update({"a.b": 1}, {$set: {"a.b": 2}}));
assert.eq(1, coll.find({"a.b": 2}).itcount());
assert.eq(1, coll.find({a: {b: 2}}).itcount());
assert.commandWorked(coll.update({"a.b": 2}, {"a.b": 3}));
assert.eq(0, coll.find({"a.b": 3}).itcount());
// Upserting _id fields containing $-prefixed fields is not allowed.
assert.writeErrorWithCode(
coll.update({"a.b": 1}, {_id: {$invalid: 1}}, {upsert: true}),
ErrorCodes.DollarPrefixedFieldName,
);
assert.writeErrorWithCode(
coll.update({"a.b": 1}, {$set: {_id: {$invalid: 1}}}, {upsert: true}),
ErrorCodes.DollarPrefixedFieldName,
);
assert.writeErrorWithCode(
coll.update({"a.b": 1}, {$set: {"_id.$gt": 1}}, {upsert: true}),
ErrorCodes.DollarPrefixedFieldName,
);
assert.writeErrorWithCode(
coll.update({"a.b": 1}, {$setOnInsert: {_id: {$invalid: 1}}}, {upsert: true}),
ErrorCodes.DollarPrefixedFieldName,
);
assert.writeErrorWithCode(
coll.update({"a.b": 1}, {$setOnInsert: {"_id.$invalid": 1}}, {upsert: true}),
ErrorCodes.DollarPrefixedFieldName,
);
assert.writeErrorWithCode(
coll.update({"_id.$gt": 1}, {$set: {a: 1}}, {upsert: true}),
ErrorCodes.DollarPrefixedFieldName,
);
// Replacement-style updates can contain nested $-prefixed fields.
assert.commandWorked(coll.update({"a.b": 1}, {a: {$c: 1}}));
assert.commandWorked(coll.update({"a.b": 2}, {a: {$c: 1}}, {upsert: true}));
assert.commandWorked(coll.update({"a.b": 1}, {foo: {$c: 1, bar: {$d: 4}}}));
// Pipeline-style updates are allowed to contain $-prefixed fields.
assert.commandWorked(coll.update({"a.b": 1}, [{$replaceWith: {$literal: {$a: 1}}}]));
assert.commandWorked(coll.update({"a.b": 3}, [{$replaceWith: {$literal: {a: {$a: 1}}}}], {upsert: true}));
assert.commandWorked(coll.update({"a.b": 1}, [{$replaceWith: {$literal: {a: {$a: 1}}}}]));
// Pipeline-style updates are allowed to contain reserved $-prefixed fields.
assert.commandWorked(coll.update({"a.b": 1}, [{$replaceWith: {$literal: {$ref: 1}}}]));
assert.commandWorked(coll.update({"a.b": 4}, [{$replaceWith: {$literal: {$ref: 1}}}], {upsert: true}));
assert.commandWorked(coll.update({"a.b": 5}, [{$replaceWith: {$literal: {$id: 1}}}], {upsert: true}));
assert.commandWorked(coll.update({"a.b": 6}, [{$replaceWith: {$literal: {$db: 1}}}], {upsert: true}));
// $-prefixed field names are not allowed at the top-level in replacement-style updates.
assert.writeErrorWithCode(coll.update({"a.b": 1}, {$c: 1}), ErrorCodes.FailedToParse);
// Top-level reserved $-prefixed field names are not allowed in replacement-style updates.
assert.writeErrorWithCode(coll.update({"a.b": 1}, {$ref: 1}), ErrorCodes.FailedToParse);
assert.writeErrorWithCode(coll.update({"a.b": 1}, {$id: 1}), ErrorCodes.FailedToParse);
assert.writeErrorWithCode(coll.update({"a.b": 1}, {$db: 1}), ErrorCodes.FailedToParse);
// Nested reserved $-prefixed field names are not allowed in replacement-style updates.
assert.commandWorked(coll.update({"a.b": 1}, {a: {$ref: 1}}));
assert.commandWorked(coll.update({"a.b": 1}, {b: {$id: 1}}));
assert.commandWorked(coll.update({"a.b": 1}, {c: {$db: 1}}));
// Push update modifier can sort on $-prefixed field
assert.commandWorked(coll.update({"a.b": 1}, {$push: {array: {$each: [{a: 4, "$b": 5}], $sort: {"$b": 1}}}}));
assert.commandWorked(coll.update({"a.b": 1}, {$push: {array: {$each: [{a: {"$b": 5}}], $sort: {"a.$b": 1}}}}));
assert.commandWorked(coll.update({"a.b": 1}, {$push: {array: {$each: [{"$a": {"$b": 5}}], $sort: {"$a.$b": 1}}}}));
// sortArray in an update modifier can sort on a $-prefixed field
assert.commandWorked(coll.update({"a.b": 1}, {f: {$set: {$sortArray: {input: "$f", sortBy: {"$g": 1}}}}}));
//
// FindAndModify field name validation.
//
coll.drop();
// Dotted fields are allowed in update object.
coll.findAndModify({query: {_id: 0}, update: {_id: 0, "a.b": 1}, upsert: true});
assert.eq([{_id: 0, "a.b": 1}], coll.find({_id: 0}).toArray());
// Dotted fields represent paths in $set.
coll.findAndModify({query: {_id: 1}, update: {$set: {_id: 1, "a.b": 1}}, upsert: true});
assert.eq([{_id: 1, a: {b: 1}}], coll.find({_id: 1}).toArray());
// Dotted fields represent paths in the query object.
coll.findAndModify({query: {_id: 0, "a.b": 1}, update: {"a.b": 2}});
assert.eq([{_id: 0, "a.b": 1}], coll.find({_id: 0}).toArray());
coll.findAndModify({query: {_id: 1, "a.b": 1}, update: {$set: {_id: 1, "a.b": 2}}});
assert.eq([{_id: 1, a: {b: 2}}], coll.find({_id: 1}).toArray());
// Dotted fields in a $literal-wrapped document can be updated in a pipeline-style update.
coll.findAndModify({query: {_id: 1}, update: [{$replaceWith: {$literal: {_id: 1, "a.b": 3}}}]});
assert.eq([{_id: 1, "a.b": 3}], coll.find({_id: 1}).toArray());
coll.findAndModify({query: {_id: 1}, update: [{$replaceWith: {$literal: {_id: 1, "a.b.c": 3}}}]});
assert.eq([{_id: 1, "a.b.c": 3}], coll.find({_id: 1}).toArray());
// Dotted fields without $literal cannot be updated in a pipeline-style update.
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: [{$replaceWith: {_id: 1, "a.b.": 2}}]});
});
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: [{$replaceWith: {_id: 1, "a.b.c": 3}}]});
});
// Top-level $-prefixed field names are not allowed in a replacement-style update.
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: {_id: 1, $invalid: 1}});
});
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: {$set: {_id: 1, $invalid: 1}}});
});
// Reserved $-prefixed field names are not allowed in a replacement-style update.
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: {_id: 1, $ref: 1}});
});
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: {_id: 1, $id: 1}});
});
assert.throws(function () {
coll.findAndModify({query: {_id: 1}, update: {_id: 1, $db: 1}});
});
// Test that $-prefixed fields are allowed when they are not at the top-level.
coll.findAndModify({query: {_id: 2}, update: {_id: 2, out: {$in: 1, "x": 2}}, upsert: true});
assert([{_id: 2, out: {$in: 1, "x": 2}}], coll.find({_id: 2}).toArray());
// Test that $-prefixed fields are allowed when a pipeline is used.
coll.findAndModify({
query: {_id: 2},
update: [{$replaceWith: {$literal: {_id: 2, $in: 2, "x": 3}}}],
upsert: true,
});
assert([{_id: 2, $in: 2, "x": 3}], coll.find({_id: 2}).toArray());
// Reserved $-prefixed field names are allowed when they are nested.
coll.findAndModify({query: {_id: 1}, update: {_id: 1, a: {$ref: 1}}});
assert.eq([{_id: 1, a: {$ref: 1}}], coll.find({_id: 1}).toArray());
coll.findAndModify({query: {_id: 1}, update: {_id: 1, a: {$id: 1}}});
assert.eq([{_id: 1, a: {$id: 1}}], coll.find({_id: 1}).toArray());
coll.findAndModify({query: {_id: 1}, update: {_id: 1, a: {$db: 1}}});
assert.eq([{_id: 1, a: {$db: 1}}], coll.find({_id: 1}).toArray());
//
// Aggregation field name validation.
//
coll.drop();
assert.commandWorked(coll.insert({_id: {a: 1, b: 2}, "c.d": 3, "$e": 4, "f": [{"$g": 5}, {"$g": 3}]}));
// Dotted fields represent paths in an aggregation pipeline.
assert.eq(coll.aggregate([{$match: {"_id.a": 1}}, {$project: {"_id.b": 1}}]).toArray(), [{_id: {b: 2}}]);
assert.eq(coll.aggregate([{$match: {"c.d": 3}}, {$project: {"_id.b": 1}}]).toArray(), []);
assert.eq(coll.aggregate([{$project: {"_id.a": 1}}]).toArray(), [{_id: {a: 1}}]);
assert.eq(coll.aggregate([{$project: {"c.d": 1, _id: 0}}]).toArray(), [{}]);
assert.eq(
coll
.aggregate([{$addFields: {"new.field": {$multiply: ["$c.d", "$_id.a"]}}}, {$project: {"new.field": 1, _id: 0}}])
.toArray(),
[{new: {field: null}}],
);
assert.eq(coll.aggregate([{$group: {_id: "$_id.a", e: {$sum: "$_id.b"}}}]).toArray(), [{_id: 1, e: 2}]);
assert.eq(coll.aggregate([{$group: {_id: "$_id.a", e: {$sum: "$c.d"}}}]).toArray(), [{_id: 1, e: 0}]);
// Accumulation statements cannot have a dotted field name.
assert.commandFailed(
db.runCommand({aggregate: coll.getName(), pipeline: [{$group: {_id: "$_id.a", "e.f": {$sum: "$_id.b"}}}]}),
);
// $-prefixed field names are not allowed in an aggregation pipeline.
assert.commandFailed(db.runCommand({aggregate: coll.getName(), pipeline: [{$match: {"$invalid": 1}}]}));
assert.commandFailed(
db.runCommand({
aggregate: coll.getName(),
pipeline: [{$project: {"_id.a": 1, "$newField": {$multiply: ["$_id.b", "$_id.a"]}}}],
}),
);
assert.commandFailed(
db.runCommand({
aggregate: coll.getName(),
pipeline: [{$addFields: {"_id.a": 1, "$newField": {$multiply: ["$_id.b", "$_id.a"]}}}],
}),
);
assert.commandFailed(
db.runCommand({
aggregate: coll.getName(),
pipeline: [{$group: {_id: "$_id.a", "$invalid": {$sum: "$_id.b"}}}],
}),
);
//TODO(SERVER-114788): $-prefixed field names are not supported in aggregation sortBy specification
assert.commandFailed(
db.runCommand({
aggregate: coll.getName(),
pipeline: [{$group: {_id: "$_id.a", e: {$top: {output: "$_id.b", sortBy: {"$e": 1}}}}}],
}),
);
// $-prefixed field names are supported in $sortArray sort specifications in aggregation and update
assert.eq(coll.aggregate([{$project: {_id: 1, e: {$sortArray: {input: "$f", sortBy: {"$g": 1}}}}}]).toArray(), [
{_id: {a: 1, b: 2}, e: [{"$g": 3}, {"$g": 5}]},
]);