mirror of https://github.com/mongodb/mongo
523 lines
14 KiB
JavaScript
523 lines
14 KiB
JavaScript
/**
|
|
* Test pipelines with $projects/$addFields mixed together with other stages, and verify these
|
|
* pipelines produce correct results and correct plans.
|
|
*/
|
|
const coll = db.complex_pipelines;
|
|
const collName = db.getName();
|
|
const foreignColl = db.complex_pipelines_foreign;
|
|
const foreignCollName = foreignColl.getName();
|
|
|
|
coll.drop();
|
|
foreignColl.drop();
|
|
|
|
Random.setRandomSeed(20240725);
|
|
|
|
const indexSpec = {
|
|
_id: 1,
|
|
y: 1,
|
|
x: 1,
|
|
w: 1,
|
|
l: 1,
|
|
k: 1,
|
|
j: 1,
|
|
i: 1,
|
|
h: 1,
|
|
g: 1,
|
|
f: 1,
|
|
e: 1,
|
|
d: 1,
|
|
c: 1,
|
|
b: 1,
|
|
a: 1,
|
|
};
|
|
|
|
let docs = [];
|
|
let foreignDocs = [];
|
|
|
|
function tojsoncompact(obj) {
|
|
let str = tojsononeline(obj);
|
|
str = str.replaceAll(" ", "").replaceAll('"', "");
|
|
str = str.replaceAll(",", ", ").replaceAll(":", ": ");
|
|
return str;
|
|
}
|
|
|
|
// Generate 8 documents in 'docs' and 8 documents in 'foreignDocs'.
|
|
for (let i = 1; i <= 8; ++i) {
|
|
// Add a document to 'docs'. For each document, most field values are integers, except for
|
|
// fields 'w', 'x', and 'y' which are objects (each with 3 subfields 'a', 'b', and 'c').
|
|
let doc = {_id: i};
|
|
|
|
let k = Random.randInt(11);
|
|
let kStep = Random.randInt(10) + 1;
|
|
for (let j = 0; j < 11; ++j) {
|
|
if (k < 9) {
|
|
let c = String.fromCharCode(97 + k);
|
|
doc[c] = Random.randInt(100);
|
|
} else {
|
|
let c = String.fromCharCode(119 + k - 9);
|
|
doc[c] = {a: Random.randInt(100), b: Random.randInt(100), c: Random.randInt(100)};
|
|
}
|
|
|
|
k = (k + kStep) % 11;
|
|
}
|
|
|
|
docs.push(doc);
|
|
|
|
// Add a document to 'foreignDocs'.
|
|
doc = {_id: i};
|
|
|
|
k = Random.randInt(3);
|
|
kStep = Random.randInt(2) + 1;
|
|
for (let j = 0; j < 3; ++j) {
|
|
let c = String.fromCharCode(97 + k);
|
|
k = (k + kStep) % 3;
|
|
|
|
doc[c] = Random.randInt(100);
|
|
}
|
|
|
|
foreignDocs.push(doc);
|
|
}
|
|
|
|
// possibleIntFields and possibleObjFields are global variables used in the buildXXX() methods below
|
|
// to pick the fields to be used in each operation. The two arrays should be shuffled using the
|
|
// shuffleArray method before each call to one of the addXXX() methods to randomise the order of the
|
|
// fields.
|
|
let possibleIntFields = [];
|
|
let possibleObjFields = [];
|
|
|
|
for (let j = 0; j < 12; ++j) {
|
|
possibleIntFields.push(String.fromCharCode(97 + j));
|
|
}
|
|
for (let j = 0; j < 3; ++j) {
|
|
possibleObjFields.push(String.fromCharCode(119 + j));
|
|
}
|
|
|
|
function shuffleArray(arr) {
|
|
let i = arr.length;
|
|
|
|
while (i != 0) {
|
|
let j = Random.randInt(i);
|
|
--i;
|
|
|
|
let value = arr[i];
|
|
arr[i] = arr[j];
|
|
arr[j] = value;
|
|
}
|
|
}
|
|
|
|
function addInclusion(pipeline, allowDottedPaths) {
|
|
let projectionDoc = {};
|
|
|
|
let n = 8 + Random.randInt(4);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleIntFields[j];
|
|
|
|
if (Random.rand() < 0.7) {
|
|
projectionDoc[c] = 1;
|
|
} else if (Random.rand() < 0.85) {
|
|
let d = String.fromCharCode(97 + Random.randInt(12));
|
|
projectionDoc[c] = "$" + d;
|
|
} else {
|
|
let num = Random.randInt(100);
|
|
projectionDoc[c] = {"$literal": num};
|
|
}
|
|
}
|
|
|
|
n = 1 + Random.randInt(2);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleObjFields[j];
|
|
|
|
if (Random.rand() < 0.5 || !allowDottedPaths) {
|
|
projectionDoc[c] = 1;
|
|
} else {
|
|
let possibleSubfields = ["a", "b", "c"];
|
|
shuffleArray(possibleSubfields);
|
|
|
|
let m = 1 + Random.randInt(2);
|
|
for (let i = 0; i < m; ++i) {
|
|
let path = c + "." + possibleSubfields[i];
|
|
projectionDoc[path] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return pipeline.concat([{$project: projectionDoc}]);
|
|
}
|
|
|
|
function addExclusion(pipeline, allowDottedPaths) {
|
|
let projectionDoc = {};
|
|
|
|
let n = 1 + Random.randInt(2);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleIntFields[j];
|
|
projectionDoc[c] = 0;
|
|
}
|
|
|
|
n = Random.randInt(2);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleObjFields[j];
|
|
|
|
if (Random.rand() < 0.5 || !allowDottedPaths) {
|
|
projectionDoc[c] = 0;
|
|
} else {
|
|
let possibleSubfields = ["a", "b", "c"];
|
|
shuffleArray(possibleSubfields);
|
|
|
|
let m = 1 + Random.randInt(2);
|
|
for (let i = 0; i < m; ++i) {
|
|
let path = c + "." + possibleSubfields[i];
|
|
projectionDoc[path] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return pipeline.concat([{$project: projectionDoc}]);
|
|
}
|
|
|
|
function addAddFields(pipeline, allowDottedPaths = true) {
|
|
let addFieldsDoc = {};
|
|
|
|
let n = 1 + Random.randInt(3);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleIntFields[j];
|
|
|
|
if (Random.rand() < 0.65) {
|
|
let num = Random.randInt(100);
|
|
addFieldsDoc[c] = num;
|
|
} else {
|
|
let d = String.fromCharCode(97 + Random.randInt(20));
|
|
addFieldsDoc[c] = "$" + d;
|
|
}
|
|
}
|
|
|
|
n = Random.randInt(2);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleObjFields[j];
|
|
|
|
if (Random.rand() < 0.5 || !allowDottedPaths) {
|
|
let d = String.fromCharCode(119 + Random.randInt(3));
|
|
|
|
addFieldsDoc[c] = "$" + d;
|
|
} else {
|
|
let possibleSubfields = ["a", "b", "c"];
|
|
shuffleArray(possibleSubfields);
|
|
|
|
let m = 1 + Random.randInt(2);
|
|
for (let i = 0; i < m; ++i) {
|
|
let path = c + "." + possibleSubfields[i];
|
|
let num = Random.randInt(100);
|
|
|
|
addFieldsDoc[path] = num;
|
|
}
|
|
}
|
|
}
|
|
|
|
return pipeline.concat([{$addFields: addFieldsDoc}]);
|
|
}
|
|
|
|
function addMatch(pipeline, allowDottedPaths = true) {
|
|
let matchDoc = {};
|
|
|
|
if (Random.rand() < 0.5 || !allowDottedPaths) {
|
|
let c = possibleIntFields[0];
|
|
let num = Random.randInt(25);
|
|
let op = Random.randInt(4);
|
|
|
|
if (op < 2) {
|
|
num = 99 - num;
|
|
matchDoc[c] = op == 0 ? {$lt: num} : {$lte: num};
|
|
} else {
|
|
matchDoc[c] = op == 2 ? {$gt: num} : {$gte: num};
|
|
}
|
|
} else {
|
|
let possibleSubfields = ["a", "b", "c"];
|
|
shuffleArray(possibleSubfields);
|
|
|
|
let c = possibleObjFields[0];
|
|
let path = c + "." + possibleSubfields[0];
|
|
let num = Random.randInt(25);
|
|
let op = Random.randInt(4);
|
|
|
|
if (op < 2) {
|
|
num = 99 - num;
|
|
matchDoc[path] = op == 0 ? {$lt: num} : {$lte: num};
|
|
} else {
|
|
matchDoc[path] = op == 2 ? {$gt: num} : {$gte: num};
|
|
}
|
|
}
|
|
|
|
return pipeline.concat([{$match: matchDoc}]);
|
|
}
|
|
|
|
function addGroup(pipeline) {
|
|
let groupDoc = {};
|
|
|
|
let g = String.fromCharCode(97 + Random.randInt(12));
|
|
groupDoc["_id"] = "$" + g;
|
|
|
|
let n = 8 + Random.randInt(4);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleIntFields[j];
|
|
let sumArg = "$" + c;
|
|
groupDoc[c] = {$sum: sumArg};
|
|
}
|
|
|
|
for (let j = 0; j < 3; ++j) {
|
|
let c = possibleObjFields[j];
|
|
let minArg = "$" + c;
|
|
groupDoc[c] = {$min: minArg};
|
|
}
|
|
|
|
return pipeline.concat([{$group: groupDoc}]);
|
|
}
|
|
|
|
function addLookupUnwind(pipeline) {
|
|
let lookupDoc = {from: foreignCollName, localField: "_id", foreignField: "_id"};
|
|
|
|
let c = String.fromCharCode(119 + Random.randInt(3));
|
|
lookupDoc["as"] = c;
|
|
|
|
let unwindArg = "$" + c;
|
|
|
|
return pipeline.concat([{$lookup: lookupDoc}, {$unwind: unwindArg}]);
|
|
}
|
|
|
|
function addSort(pipeline, addLimit = false) {
|
|
let sortDoc = {};
|
|
|
|
let n = 1 + Random.randInt(2);
|
|
for (let j = 0; j < n; ++j) {
|
|
let c = possibleIntFields[j];
|
|
sortDoc[c] = Random.randInt(2) == 0 ? 1 : -1;
|
|
}
|
|
|
|
// Add _id to make sure that sort order is always consistent.
|
|
sortDoc["_id"] = 1;
|
|
|
|
let newPipeline = pipeline.concat([{$sort: sortDoc}]);
|
|
|
|
if (addLimit) {
|
|
let num = 5 + Random.randInt(6);
|
|
newPipeline = newPipeline.concat([{$limit: num}]);
|
|
}
|
|
|
|
return newPipeline;
|
|
}
|
|
|
|
function getRangeWindow() {
|
|
switch (Random.randInt(6)) {
|
|
case 0:
|
|
return {documents: ["unbounded", "current"]};
|
|
case 1:
|
|
return {documents: ["current", "unbounded"]};
|
|
case 2:
|
|
return {documents: ["unbounded", "unbounded"]};
|
|
case 3:
|
|
return {documents: [-2, 0]};
|
|
case 4:
|
|
return {documents: [0, 2]};
|
|
case 5:
|
|
return {documents: [-2, 2]};
|
|
}
|
|
}
|
|
|
|
function addSetWindowFields(pipeline) {
|
|
let setWindowFieldsDoc = {};
|
|
|
|
const partitionByIdx = Random.randInt(13);
|
|
if (partitionByIdx < 12) {
|
|
setWindowFieldsDoc["partitionBy"] = "$" + possibleIntFields[partitionByIdx];
|
|
}
|
|
|
|
const sortDoc = addSort([]);
|
|
const sortByDoc = Object.values(sortDoc[0])[0];
|
|
setWindowFieldsDoc["sortBy"] = sortByDoc;
|
|
|
|
shuffleArray(possibleIntFields);
|
|
shuffleArray(possibleObjFields);
|
|
|
|
let outputDoc = {};
|
|
|
|
const sumWindows = 1 + Random.randInt(2);
|
|
const minWindows = Random.randInt(2);
|
|
|
|
for (let j = 0; j < sumWindows; ++j) {
|
|
const c = possibleIntFields[j];
|
|
const arg = "$" + c;
|
|
const windowDoc = getRangeWindow();
|
|
|
|
outputDoc[c] = {$sum: arg, window: windowDoc};
|
|
}
|
|
|
|
for (let j = 0; j < minWindows; ++j) {
|
|
const c = possibleObjFields[j];
|
|
const arg = "$" + c;
|
|
const windowDoc = getRangeWindow();
|
|
outputDoc[c] = {$min: arg, window: windowDoc};
|
|
}
|
|
|
|
setWindowFieldsDoc["output"] = outputDoc;
|
|
|
|
return pipeline.concat([{$setWindowFields: setWindowFieldsDoc}]);
|
|
}
|
|
|
|
function generateTestcase({
|
|
allowInclusion,
|
|
allowGroup,
|
|
allowLookup,
|
|
allowSort,
|
|
allowSortWithLimit,
|
|
allowSetWindowFields,
|
|
allowDottedPaths,
|
|
}) {
|
|
// Initialize 'pipeline'.
|
|
let pipeline = [];
|
|
let numStages = Random.randInt(9) + 4;
|
|
|
|
for (let i = 0; i < numStages; ) {
|
|
let r = Random.rand();
|
|
shuffleArray(possibleIntFields);
|
|
shuffleArray(possibleObjFields);
|
|
|
|
if (r < 0.1) {
|
|
if (allowInclusion !== true) {
|
|
continue;
|
|
}
|
|
pipeline = addInclusion(pipeline, allowDottedPaths === true);
|
|
} else if (r < 0.2) {
|
|
pipeline = addExclusion(pipeline, allowDottedPaths === true);
|
|
} else if (r < 0.4) {
|
|
pipeline = addAddFields(pipeline, allowDottedPaths === true);
|
|
} else if (r < 0.55) {
|
|
pipeline = addMatch(pipeline, allowDottedPaths === true);
|
|
} else if (r < 0.7) {
|
|
if (allowGroup !== true) {
|
|
continue;
|
|
}
|
|
|
|
pipeline = addGroup(pipeline);
|
|
} else if (r < 0.8) {
|
|
if (allowSetWindowFields !== true) {
|
|
continue;
|
|
}
|
|
pipeline = addSetWindowFields(pipeline, allowDottedPaths === true);
|
|
} else if (r < 0.9) {
|
|
if (allowLookup !== true) {
|
|
continue;
|
|
}
|
|
|
|
pipeline = addLookupUnwind(pipeline);
|
|
} else {
|
|
if (allowSort !== true) {
|
|
continue;
|
|
}
|
|
|
|
if (r < 0.95 || allowSortWithLimit !== true) {
|
|
pipeline = addSort(pipeline);
|
|
} else {
|
|
pipeline = addSort(pipeline, true);
|
|
}
|
|
}
|
|
|
|
++i;
|
|
}
|
|
|
|
return pipeline;
|
|
}
|
|
|
|
let testcases = [];
|
|
let testcaseId = 1;
|
|
|
|
for (let k = 0; k < 2; ++k) {
|
|
let allowDottedPaths = k >= 1;
|
|
|
|
for (let j = 0; j < 3; ++j) {
|
|
let allowSort = j >= 1;
|
|
let allowSortWithLimit = j >= 2;
|
|
|
|
for (let i = 0; i < 12; ++i) {
|
|
let allowInclusion = (i >= 3 && i < 6) || i >= 9;
|
|
let allowGroup = i >= 6;
|
|
let allowLookup = i >= 9;
|
|
let allowSetWindowFields = i % 5 === 0;
|
|
|
|
let testcase = generateTestcase({
|
|
allowInclusion,
|
|
allowGroup,
|
|
allowLookup,
|
|
allowSort,
|
|
allowSortWithLimit,
|
|
allowSetWindowFields,
|
|
allowDottedPaths,
|
|
});
|
|
|
|
testcases.push({id: testcaseId, pipeline: testcase});
|
|
++testcaseId;
|
|
}
|
|
}
|
|
}
|
|
|
|
print("Docs:\n\n");
|
|
|
|
for (let i = 0; i < docs.length; ++i) {
|
|
let doc = docs[i];
|
|
let comma = i != docs.length - 1 ? "," : "";
|
|
print(tojsoncompact(doc) + comma);
|
|
}
|
|
|
|
print("\n\nForeign docs:\n\n");
|
|
|
|
for (let i = 0; i < foreignDocs.length; ++i) {
|
|
let doc = foreignDocs[i];
|
|
let comma = i != foreignDocs.length - 1 ? "," : "";
|
|
print(tojsoncompact(doc) + comma);
|
|
}
|
|
|
|
print("\n\n");
|
|
|
|
function compareResultEntries(lhs, rhs) {
|
|
const lhsJson = tojsononeline(lhs);
|
|
const rhsJson = tojsononeline(rhs);
|
|
return lhsJson < rhsJson ? -1 : lhsJson > rhsJson ? 1 : 0;
|
|
}
|
|
|
|
function runTest(testcase, useIndex) {
|
|
let testcaseId = testcase.id.toString();
|
|
let pipeline = testcase.pipeline;
|
|
|
|
let useIndexText = useIndex ? "true" : "false";
|
|
print(`Query ${testcaseId} (with useIndex=${useIndexText}): ${tojsononeline(pipeline)}\n\n`);
|
|
|
|
const options = useIndex ? {hint: indexSpec} : {};
|
|
let results = coll.aggregate(pipeline, options).toArray();
|
|
results.sort(compareResultEntries);
|
|
|
|
for (let i = 0; i < results.length; ++i) {
|
|
let result = results[i];
|
|
let comma = i != results.length - 1 ? "," : "";
|
|
print(tojsoncompact(result) + comma);
|
|
}
|
|
print("\n");
|
|
}
|
|
|
|
function runTests(useIndex) {
|
|
for (let testcase of testcases) {
|
|
runTest(testcase, useIndex);
|
|
}
|
|
}
|
|
|
|
// Insert documents into 'coll' and 'foreignColl'.
|
|
assert.commandWorked(coll.insert(docs));
|
|
assert.commandWorked(foreignColl.insert(foreignDocs));
|
|
|
|
// Run all the testcases without an index.
|
|
let useIndex = false;
|
|
runTests(useIndex);
|
|
|
|
// Create an index.
|
|
assert.commandWorked(coll.createIndex(indexSpec));
|
|
|
|
// Run all the testcases _with_ an index.
|
|
useIndex = true;
|
|
runTests(useIndex);
|