mirror of https://github.com/mongodb/mongo
243 lines
9.2 KiB
JavaScript
243 lines
9.2 KiB
JavaScript
/**
|
|
* This is a property-based test for the Express execution path. It
|
|
* tests that query results match when using the express path and when we
|
|
* include a hint(), which disables the express path.
|
|
*
|
|
* It also verifies that _id-based update() and remove() operations
|
|
* perform as expected.
|
|
*
|
|
* @tags: [
|
|
* requires_fcv_80,
|
|
* requires_getmore,
|
|
* # TODO SERVER-115270 This test hits the evergreen timeout when run in
|
|
* # suites with additional overhead.
|
|
* query_intensive_pbt,
|
|
* ]
|
|
*/
|
|
|
|
import {assertDropAndRecreateCollection} from "jstests/libs/collection_drop_recreate.js";
|
|
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
|
|
import {isSlowBuild} from "jstests/libs/query/aggregation_pipeline_utils.js";
|
|
import {fc} from "jstests/third_party/fast_check/fc-3.1.0.js";
|
|
|
|
if (isSlowBuild(db)) {
|
|
jsTest.log.info("Exiting early because debug is on, opt is off, or a sanitizer is enabled.");
|
|
quit();
|
|
}
|
|
|
|
const fieldArb = fc.constantFrom("_id", "a", "b", "c", "d", "e", "f");
|
|
const projectFieldsArb = fc.uniqueArray(fieldArb, {minLength: 0, maxLength: 7});
|
|
const projectArb = fc
|
|
.record({fields: projectFieldsArb, idIncluded: fc.boolean(), isInclusive: fc.boolean()})
|
|
.map(function ({fields, idIncluded, isInclusive}) {
|
|
const projectList = {};
|
|
for (const field of fields) {
|
|
projectList[field] = field === "_id" ? idIncluded : isInclusive;
|
|
}
|
|
return projectList;
|
|
});
|
|
|
|
const directionArb = fc.constantFrom(1, -1);
|
|
const indexSpecArb = fc.dictionary(fieldArb, directionArb, {minKeys: 1, maxKeys: 8});
|
|
const fieldValueArb = fc.oneof(
|
|
fc.boolean(),
|
|
fc.integer({min: 1, max: 10}),
|
|
fc.constantFrom("foo", "bar", "baz"),
|
|
fc.date({min: new Date("1991-01-01T00:00:00.000Z"), max: new Date("2001-01-01T00:00:00.000Z")}),
|
|
fc.constant(null),
|
|
);
|
|
|
|
const arrayFieldValueArb = fc.oneof(
|
|
fc.array(fc.boolean(), {maxLength: 3}),
|
|
fc.array(fc.integer(), {maxLength: 3}),
|
|
fc.array(fc.constantFrom("foo", "bar", "baz", ""), {maxLength: 4}),
|
|
fc.array(fc.date({min: new Date("1991-01-01T00:00:00.000Z"), max: new Date("2001-01-01T00:00:00.000Z")}), {
|
|
maxLength: 4,
|
|
}),
|
|
);
|
|
|
|
const documentArb = fc.record(
|
|
{
|
|
_id: fieldValueArb.filter((val) => val !== null), // _id cannot be null
|
|
a: arrayFieldValueArb,
|
|
b: fieldValueArb,
|
|
c: fieldValueArb,
|
|
d: fieldValueArb,
|
|
e: fieldValueArb,
|
|
f: fieldValueArb,
|
|
},
|
|
{
|
|
noNullPrototype: false,
|
|
},
|
|
);
|
|
|
|
// Arbitrary for all documents in the collection.
|
|
const docsArb = fc.array(documentArb, {minLength: 1, maxLength: 5});
|
|
const updateValueArb = fc.oneof(
|
|
fc.integer({min: 10, max: 30}),
|
|
fc.constantFrom("bee", "biz", "dog1"),
|
|
fc.date({min: new Date("2002-01-01T00:00:00.000Z"), max: new Date("2026-01-01T00:00:00.000Z")}),
|
|
);
|
|
|
|
const testCaseArb = fc.record({
|
|
indexSpec: indexSpecArb,
|
|
docs: docsArb,
|
|
projectSpec: projectArb,
|
|
isIndexUnique: fc.boolean(),
|
|
isClustered: fc.boolean(),
|
|
updateValue: updateValueArb,
|
|
});
|
|
|
|
function hasDuplicates(arr) {
|
|
return new Set(arr).size !== arr.length;
|
|
}
|
|
|
|
function arrayContainsElement(arr, elem) {
|
|
if (!Array.isArray(elem)) {
|
|
elem = [elem];
|
|
}
|
|
if (_resultSetsEqualUnordered(arr, elem)) {
|
|
return true;
|
|
}
|
|
for (const a of arr) {
|
|
if (_resultSetsEqualUnordered([a], elem)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function verifyReadOperations(collection, query, projectSpec) {
|
|
const expressRes = collection.find(query, projectSpec).toArray();
|
|
const fallbackRes = collection.find(query, projectSpec).hint({_id: 1}).toArray();
|
|
let agg = [{$match: query}];
|
|
if (Object.keys(projectSpec).length > 0) {
|
|
agg.push({$project: projectSpec});
|
|
}
|
|
const expressAggRes = collection.aggregate(agg).toArray();
|
|
assert(_resultSetsEqualUnordered(expressRes, fallbackRes));
|
|
assert(_resultSetsEqualUnordered(expressAggRes, fallbackRes));
|
|
|
|
// adding limit(1) enables some additional queries to become Express
|
|
// eligible, but since we can't have a sort(), we check against all possible
|
|
// results.
|
|
const expressLimitRes = collection.find(query, projectSpec).limit(1).toArray();
|
|
agg.push({$limit: 1});
|
|
const expressLimitAggRes = collection.aggregate(agg).toArray();
|
|
const fallbackLimitRes = collection.find(query, projectSpec).hint({_id: 1}).toArray();
|
|
|
|
assert(arrayContainsElement(fallbackLimitRes, expressLimitRes), [fallbackLimitRes, expressLimitRes]);
|
|
assert(arrayContainsElement(fallbackLimitRes, expressLimitAggRes), [fallbackLimitRes, expressLimitAggRes]);
|
|
}
|
|
|
|
function verifyWriteOperations(collection, query, updateValue) {
|
|
// Only test _id queries, since Express write operations don't support non-_id
|
|
// indexes.
|
|
if (query.hasOwnProperty("_id")) {
|
|
let updateRes = collection.find(query).toArray()[0];
|
|
updateRes.newField = updateValue;
|
|
assert.commandWorked(collection.update(query, updateRes));
|
|
updateRes = collection.find(query).toArray()[0];
|
|
assert.eq(updateRes.newField, updateValue, updateRes);
|
|
|
|
assert.commandWorked(collection.deleteOne(query));
|
|
assert.eq(collection.find(query).toArray(), []);
|
|
}
|
|
}
|
|
|
|
// Malformed predicates like {_id: 1, _id: 2} should either error out
|
|
// or be ignored (server behavior is undefined, but obviously should not crash).
|
|
function verifyInvalidWriteOperations(collection, query, updateValue) {
|
|
const errors = [11000, 9248801, 9248804];
|
|
assert.commandWorkedOrFailedWithCode(collection.update(query, updateValue), errors);
|
|
assert.commandWorkedOrFailedWithCode(collection.deleteOne(query), errors);
|
|
}
|
|
|
|
fc.assert(
|
|
fc.property(testCaseArb, ({indexSpec, docs, projectSpec, isIndexUnique, isClustered, updateValue}) => {
|
|
const fields = Object.keys(indexSpec);
|
|
fc.pre(!hasDuplicates(docs.map((doc) => doc._id.toString())));
|
|
|
|
fc.pre(
|
|
!isIndexUnique ||
|
|
!hasDuplicates(
|
|
docs
|
|
.map((doc) => {
|
|
let d = doc[fields[0]];
|
|
if (d == null || typeof d == "undefined") {
|
|
return "null";
|
|
} else if (Array.isArray(d) && d.length == 0) {
|
|
return "[]";
|
|
} else if (typeof d.getTime === "function") {
|
|
return d.getTime();
|
|
} else {
|
|
return d;
|
|
}
|
|
})
|
|
.flat(),
|
|
),
|
|
);
|
|
|
|
const collName = jsTestName();
|
|
if (isClustered) {
|
|
assertDropAndRecreateCollection(db, collName, {clusteredIndex: {key: {_id: 1}, unique: true}});
|
|
} else {
|
|
assertDropAndRecreateCollection(db, collName);
|
|
}
|
|
const coll = db[collName];
|
|
|
|
// don't create duplicative _id-only index
|
|
if (!indexSpec.hasOwnProperty("_id") || fields.length != 1) {
|
|
if (isIndexUnique && FixtureHelpers.isSharded(coll)) {
|
|
isIndexUnique = false; // sharded collections can't have unique indexes
|
|
}
|
|
assert.commandWorked(coll.createIndex(indexSpec, {unique: isIndexUnique}));
|
|
}
|
|
assert.commandWorked(coll.insert(docs));
|
|
|
|
let queryList = [],
|
|
invalidQueryList = [];
|
|
const indexes = ["_id", fields[0]];
|
|
indexes.forEach((curField) => {
|
|
const value = docs[0][curField];
|
|
queryList.push({[curField]: value});
|
|
queryList.push({[curField]: {$eq: value}});
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
queryList.push({[curField]: value[0]});
|
|
queryList.push({[curField]: {$eq: value[0]}});
|
|
}
|
|
// test queries with no expected matches too
|
|
queryList.push({[curField]: "no match"});
|
|
queryList.push({[curField]: 123});
|
|
queryList.push({[curField]: {$eq: 123}});
|
|
// test queries with invalid predicates
|
|
const cfs = String([curField]);
|
|
invalidQueryList.push(_buildBsonObj(cfs, value, cfs, value));
|
|
invalidQueryList.push(_buildBsonObj(cfs, value, cfs, 0));
|
|
invalidQueryList.push(_buildBsonObj(cfs, {$eq: value}, cfs, 0));
|
|
});
|
|
|
|
jsTest.log.info("Generated test data", {docs, indexSpec, projectSpec});
|
|
|
|
queryList.forEach((query) => {
|
|
jsTest.log.info("Verifying read operation", {query});
|
|
verifyReadOperations(coll, query, projectSpec);
|
|
});
|
|
|
|
// queryList[0] should always produce exactly 1 match.
|
|
jsTest.log.info("Verifying write operation", {query: queryList[0], update: updateValue});
|
|
verifyWriteOperations(coll, queryList[0], updateValue);
|
|
|
|
invalidQueryList.forEach((query) => {
|
|
verifyReadOperations(coll, query, projectSpec);
|
|
verifyInvalidWriteOperations(coll, query, {newField: updateValue});
|
|
});
|
|
}),
|
|
{
|
|
seed: 413,
|
|
// The search space for this PBT is small because express path covers a narrow range of
|
|
// queries. 300 runs should be enough.
|
|
numRuns: 300,
|
|
},
|
|
);
|