mongo/jstests/query_golden/complex_pipelines.js

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);