EXPLAIN OUTPUT: "explainVersion" : "2", "stages" : [ { "$cursor" : { "queryPlanner" : { "namespace" : "fuzzer.system.buckets.fuzzer_coll", "parsedQuery" : { "control.min.obj.obj.str" : { "$_internalExprLte" : "structure" } }, "indexFilterSet" : false, "queryHash" : "FFB0044E", "planCacheKey" : "86B4EBDC", "optimizationTimeMillis" : 12, "maxIndexedOrSolutionsReached" : false, "maxIndexedAndSolutionsReached" : false, "maxScansToExplodeReached" : false, "prunedSimilarIndexes" : false, "winningPlan" : { "isCached" : false, "queryPlan" : { "stage" : "PROJECTION_DEFAULT", "planNodeId" : 4, "transformBy" : { "_id" : true, "obj" : { "obj" : { "obj" : { "obj" : { "obj" : true } }, "date" : { "$const" : ISODate("0001-01-01T00:00:00Z") } } } }, "inputStage" : { "stage" : "UNPACK_TS_BUCKET", "planNodeId" : 3, "include" : [ "_id", "obj" ], "computedMetaProjFields" : [ ], "includeMeta" : false, "eventFilter" : { "obj.obj.str" : { "$lte" : "structure" } }, "wholeBucketFilter" : { }, "inputStage" : { "stage" : "FETCH", "planNodeId" : 2, "inputStage" : { "stage" : "IXSCAN", "planNodeId" : 1, "keyPattern" : { "control.min.obj.obj.str" : 1, "control.max.obj.obj.str" : 1 }, "indexName" : "obj.obj.str_1", "isMultiKey" : false, "multiKeyPaths" : { "control.min.obj.obj.str" : [ ], "control.max.obj.obj.str" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "control.min.obj.obj.str" : [ "[MinKey, \"structure\"]" ], "control.max.obj.obj.str" : [ "[MinKey, MaxKey]" ] } } } } }, "slotBasedPlan" : { "slots" : "$$RESULT=s17 env: { s5 = {\"control.min.obj.obj.str\" : 1, \"control.max.obj.obj.str\" : 1}, s13 = Nothing (nothing) }", "stages" : "[4] project [s17 = makeBsonObj(MakeObjSpec([_id, obj = MakeObj([obj = MakeObj([obj = MakeObj([obj = MakeObj([obj], Closed, RetNothing)], Closed, RetNothing), date = Add(0)], Closed)], Closed)], [_id, obj], Closed), null, true, s15, s16, s14)] \n[4] block_to_row blocks[s8, s9] row[s15, s16] s12 \n[4] project [s14 = Date(-62135596800000)] \n[3] filter {!(valueBlockNone(s12, true))} \n[3] project [s12 = valueBlockLogicalAnd(s11, cellFoldValues_F(valueBlockFillEmpty(valueBlockLteScalar(cellBlockGetFlatValuesBlock(s10), \"structure\"), false), s10))] \n[3] ts_bucket_to_cellblock s6 pathReqs[s8 = ProjectPath(Get(_id)/Id), s9 = ProjectPath(Get(obj)/Id), s10 = FilterPath(Get(obj)/Traverse/Get(obj)/Traverse/Get(str)/Traverse/Id)] bitmap = s11 \n[2] nlj inner [] [s1, s2, s3, s4, s5] \n left \n [1] ixseek KS(0A0A0104) KS(3C73747275637475726500F0FE04) s4 s1 s2 s3 [] @\"98ef2952-3978-4d6e-b05c-320c484d2411\" @\"obj.obj.str_1\" true \n right \n [2] limit 1ll \n [2] seek s1 s6 s7 s2 s3 s4 s5 none none [] @\"98ef2952-3978-4d6e-b05c-320c484d2411\" true false \n" } }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true, "nReturned" : 3, "executionTimeMillis" : 28, "totalKeysExamined" : 3, "totalDocsExamined" : 3, "executionStages" : { "stage" : "project", "planNodeId" : 4, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "projections" : { "17" : "makeBsonObj(MakeObjSpec([_id, obj = MakeObj([obj = MakeObj([obj = MakeObj([obj = MakeObj([obj], Closed, RetNothing)], Closed, RetNothing), date = Add(0)], Closed)], Closed)], [_id, obj], Closed), null, true, s15, s16, s14) " }, "inputStage" : { "stage" : "block_to_row", "planNodeId" : 4, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "inputStage" : { "stage" : "project", "planNodeId" : 4, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "projections" : { "14" : "Date(-62135596800000) " }, "inputStage" : { "stage" : "filter", "planNodeId" : 3, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "numTested" : 3, "filter" : "!(valueBlockNone(s12, true)) ", "inputStage" : { "stage" : "project", "planNodeId" : 3, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "projections" : { "12" : "valueBlockLogicalAnd(s11, cellFoldValues_F(valueBlockFillEmpty(valueBlockLteScalar(cellBlockGetFlatValuesBlock(s10), \"structure\"), false), s10)) " }, "inputStage" : { "stage" : "ts_bucket_to_cellblock", "planNodeId" : 3, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "numCellBlocksProduced" : 9, "numStorageBlocks" : 6, "numStorageBlocksDecompressed" : 6, "inputStage" : { "stage" : "nlj", "planNodeId" : 2, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "totalDocsExamined" : 3, "totalKeysExamined" : 3, "collectionScans" : 0, "collectionSeeks" : 3, "indexScans" : 0, "indexSeeks" : 1, "indexesUsed" : [ "obj.obj.str_1" ], "innerOpens" : 3, "innerCloses" : 1, "outerProjects" : [ ], "outerCorrelated" : [ NumberLong(1), NumberLong(2), NumberLong(3), NumberLong(4), NumberLong(5) ], "outerStage" : { "stage" : "ixseek", "planNodeId" : 1, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 1, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "indexName" : "obj.obj.str_1", "keysExamined" : 3, "seeks" : 1, "numReads" : 4, "indexKeySlot" : 4, "recordIdSlot" : 1, "snapshotIdSlot" : 2, "indexIdentSlot" : 3, "outputSlots" : [ ], "indexKeysToInclude" : "00000000000000000000000000000000", "seekKeyLow" : "KS(0A0A0104) ", "seekKeyHigh" : "KS(3C73747275637475726500F0FE04) " }, "innerStage" : { "stage" : "limit", "planNodeId" : 2, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 3, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 1, "limit" : 1, "inputStage" : { "stage" : "seek", "planNodeId" : 2, "nReturned" : 3, "executionTimeMillisEstimate" : 0, "opens" : 3, "closes" : 1, "saveState" : 2, "restoreState" : 2, "isEOF" : 0, "numReads" : 3, "recordSlot" : 6, "recordIdSlot" : 7, "seekRecordIdSlot" : 1, "snapshotIdSlot" : 2, "indexIdentSlot" : 3, "indexKeySlot" : 4, "indexKeyPatternSlot" : 5, "scanFieldNames" : [ ], "scanFieldSlots" : [ ] } } } } } } } } }, "allPlansExecution" : [ ] } }, "nReturned" : NumberLong(3), "executionTimeMillisEstimate" : NumberLong(8) }, { "$addFields" : { "obj" : { "obj" : { "obj" : { "str" : { "$const" : "OPERATIVE INFO-MEDIARIES" } } } } }, "nReturned" : NumberLong(3), "executionTimeMillisEstimate" : NumberLong(8) }, { "$sort" : { "sortKey" : { "obj" : 1, "_id" : 1 } }, "totalDataSizeSortedBytesEstimate" : NumberLong(6080), "usedDisk" : false, "spills" : NumberLong(0), "spilledDataStorageSize" : NumberLong(0), "nReturned" : NumberLong(3), "executionTimeMillisEstimate" : NumberLong(8) }, { "$_internalSetWindowFields" : { "sortBy" : { "obj" : 1 }, "output" : { "obj.obj.obj.obj.obj" : { "$last" : { "$mergeObjects" : [ "$obj", { "$const" : { "k" : "Toys", "v" : false } } ] }, "window" : { "documents" : [ "unbounded", "unbounded" ] } } } }, "maxFunctionMemoryUsageBytes" : { "obj.obj.obj.obj.obj" : NumberLong(0) }, "maxTotalMemoryUsageBytes" : NumberLong(3439), "usedDisk" : false, "nReturned" : NumberLong(3), "executionTimeMillisEstimate" : NumberLong(12) } ], "serverInfo" : { "host" : "ip-10-122-14-112", "port" : 20040, "version" : "8.1.0-alpha-3241-g6d04a8e", "gitVersion" : "6d04a8ef894d85bd5ffe4e85b1b0278f751dddb0" }, "serverParameters" : { "internalQueryFacetBufferSizeBytes" : 104857600, "internalQueryFacetMaxOutputDocSizeBytes" : 104857600, "internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600, "internalDocumentSourceGroupMaxMemoryBytes" : 104857600, "internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600, "internalQueryProhibitBlockingMergeOnMongoS" : 0, "internalQueryMaxAddToSetBytes" : 104857600, "internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600, "internalQueryFrameworkControl" : "trySbeRestricted", "internalQueryPlannerIgnoreIndexWithCollationForRegex" : 1 }, "command" : { "aggregate" : "system.buckets.fuzzer_coll", "pipeline" : [ { "$_internalUnpackBucket" : { "timeField" : "time", "metaField" : "tag", "bucketMaxSpanSeconds" : 1730115, "assumeNoMixedSchemaData" : true, "usesExtendedRange" : false, "fixedBuckets" : false } }, { "$match" : { "obj.obj.str" : { "$lte" : "structure" } } }, { "$project" : { "obj.obj.obj.obj.obj" : 1, "obj.obj.date" : ISODate("0001-01-01T00:00:00Z") } }, { "$addFields" : { "obj.obj.obj.str" : { "$toUpper" : "Operative info-mediaries" } } }, { "$setWindowFields" : { "sortBy" : { "obj" : 1 }, "output" : { "obj.obj.obj.obj.obj" : { "$last" : { "$mergeObjects" : [ "$obj", { "k" : "Toys", "v" : false } ] } } } } } ], "cursor" : { }, "collation" : { "locale" : "simple" }, "maxTimeMS" : NumberLong(30000) }, "ok" : 1 } JSTEST: const aggregationList = [ [{"$match": {"obj.obj.str": {"$lte": "structure"}}}, {"$project": {"obj.obj.obj.obj.obj": 1, "obj.obj.date": new Date('0001-01-01')}}, {"$addFields": {"obj.obj.obj.str": {"$toUpper": "Operative info-mediaries"}}}, { "$setWindowFields" : {"sortBy" : { "obj" : 1 }, "output" : { "obj.obj.obj.obj.obj" : { "$last" : { "$mergeObjects" : [ "$obj", { "k" : "Toys", "v" : false }]}}}}}], // 631 ]; const aggregationOptionsList = [ {}, // 631 ]; const collectionNames = [ 'fuzzer_coll', 'fuzzer_coll_lookup', ]; const debug = false; // 'optimization' or 'version'. const diffTestingMode = 'timeseries'; const documentList = [ {_id: 77, time: new Date("2024-01-11T08:29:54.606Z"), "obj": {_id: 78, "obj": {_id: 79, "str": "Georgia Multi-layered Upgradable", "obj": {}} } }, // 33 {_id: 98, time: new Date("2024-01-11T18:58:58.948Z"), "tag": {scientist: 0, assistant: 1, }, "obj": {_id: 99, "str": "New Leu Small Concrete Car", "obj": {_id: 100, "str": "Forint violet calculate"}}}, // 41 {_id: 128, time: new Date("2024-01-12T22:28:03.624Z"), "tag": {scientist: 2, assistant: 0, }, "obj": {_id: 132, "str": "Incredible Soft Pants Alaska Tasty", "obj": {_id: 133, "str": "Car pixel", "date": null, "obj": {_id: 134, "str": "quantify Direct", "obj": {_id: 135, "str": null}}}}}, // 59 ]; // 'expression' or 'pipeline' const fuzzerName = 'agg_fuzzer'; const indexOptionList = [ {}, // 0 {collation: {locale: 'en', strength: 2, }, }, // 1 {}, // 2 {collation: {locale: 'en', strength: 1, }, }, // 3 {}, // 4 {}, // 5 {}, // 6 {}, // 7 {collation: {locale: 'ru', strength: 4, }, }, // 8 {collation: {locale: 'en', strength: 1, }, }, // 9 {}, // 10 {}, // 11 {}, // 12 {collation: {locale: 'ru', strength: 5, }, }, // 13 {collation: {locale: 'en_US', strength: 1, }, }, // 14 {}, // 15 {collation: {locale: 'ko', strength: 1, }, }, // 16 {collation: {locale: 'en', strength: 5, }, }, // 17 {collation: {locale: 'en_US_POSIX', }, }, // 18 {}, // 19 {}, // 20 {}, // 21 {}, // 22 {}, // 23 {}, // 24 {collation: {locale: 'ko', strength: 5, }, }, // 25 {collation: {locale: 'el', strength: 3, }, }, // 26 {}, // 27 {}, // 28 {}, // 29 {collation: {locale: 'el', strength: 2, }, }, // 30 {collation: {locale: 'ko', strength: 2, }, }, // 31 {collation: {locale: 'ko', strength: 4, }, }, // 32 {}, // 33 {}, // 34 {}, // 35 {collation: {locale: 'el', strength: 5, }, }, // 36 {}, // 37 {}, // 38 {}, // 39 {collation: {locale: 'el', strength: 5, }, }, // 40 {}, // 41 {collation: {locale: 'ko', strength: 5, }, }, // 42 {}, // 43 {collation: {locale: 'en_US_POSIX', strength: 2, }, }, // 44 {}, // 45 {}, // 46 {}, // 47 {}, // 48 {}, // 49 {collation: {locale: 'en', strength: 3, }, }, // 50 {collation: {locale: 'en_US_POSIX', strength: 5, }, }, // 51 {collation: {locale: 'el', strength: 2, }, }, // 52 {collation: {locale: 'en', strength: 3, }, }, // 53 {collation: {locale: 'en', strength: 3, }, }, // 54 {}, // 55 {collation: {locale: 'en', strength: 2, }, }, // 56 {}, // 57 {collation: {locale: 'en', strength: 4, }, }, // 58 {}, // 59 {collation: {locale: 'en_US', strength: 4, }, }, // 60 {}, // 61 {collation: {locale: 'el', strength: 5, }, }, // 62 {collation: {locale: 'en_US_POSIX', strength: 3, }, }, // 63 {}, // 64 {}, // 65 {collation: {locale: 'en_US_POSIX', }, }, // 66 {}, // 67 {}, // 68 {collation: {locale: 'en', strength: 5, }, }, // 69 {collation: {locale: 'ko', strength: 2, }, }, // 70 {collation: {locale: 'el', strength: 2, }, }, // 71 {}, // 72 {}, // 73 {}, // 74 {collation: {locale: 'ko', strength: 1, }, }, // 75 {}, // 76 {}, // 77 {}, // 78 {}, // 79 {}, // 80 {}, // 81 {collation: {locale: 'en_US_POSIX', strength: 1, }, }, // 82 {}, // 83 {collation: {locale: 'ru', strength: 5, }, }, // 84 {}, // 85 {collation: {locale: 'en_US_POSIX', strength: 1, }, }, // 86 {collation: {locale: 'en_US_POSIX', strength: 3, }, }, // 87 {collation: {locale: 'ko', strength: 5, }, }, // 88 {}, // 89 {}, // 90 {}, // 91 {}, // 92 {}, // 93 {}, // 94 {collation: {locale: 'en_US_POSIX', strength: 1, }, }, // 95 {}, // 96 {}, // 97 {}, // 98 {}, // 99 {}, // 100 {}, // 101 {}, // 102 {}, // 103 {collation: {locale: 'ru', strength: 1, }, }, // 104 {}, // 105 {collation: {locale: 'en', strength: 4, }, }, // 106 {collation: {locale: 'en_US_POSIX', strength: 5, }, }, // 107 {}, // 108 {collation: {locale: 'ko', strength: 5, }, }, // 109 {}, // 110 {}, // 111 {}, // 112 {collation: {locale: 'en', strength: 5, }, }, // 113 {collation: {locale: 'ko', strength: 4, }, }, // 114 {}, // 115 {}, // 116 {}, // 117 {}, // 118 {}, // 119 {collation: {locale: 'el', strength: 2, }, }, // 120 {}, // 121 {collation: {locale: 'en_US', strength: 5, }, }, // 122 {collation: {locale: 'ru', strength: 1, }, }, // 123 {collation: {locale: 'ko', strength: 3, }, }, // 124 {}, // 125 {}, // 126 {}, // 127 {collation: {locale: 'en_US_POSIX', strength: 1, }, }, // 128 {}, // 129 {}, // 130 {}, // 131 {collation: {locale: 'ko', strength: 2, }, }, // 132 {}, // 133 {}, // 134 {}, // 135 {}, // 136 {collation: {locale: 'en', strength: 4, }, }, // 137 {collation: {locale: 'en', strength: 1, }, }, // 138 {}, // 139 {}, // 140 {}, // 141 {}, // 142 {}, // 143 {}, // 144 {}, // 145 {}, // 146 {}, // 147 {}, // 148 {}, // 149 {}, // 150 {collation: {locale: 'en', strength: 2, }, }, // 151 {collation: {locale: 'el', strength: 1, }, }, // 152 {}, // 153 {}, // 154 {collation: {locale: 'ko', strength: 5, }, }, // 155 {}, // 156 {}, // 157 {}, // 158 {}, // 159 {}, // 160 {}, // 161 {}, // 162 {collation: {locale: 'en_US', }, }, // 163 {collation: {locale: 'en_US', strength: 2, }, }, // 164 {collation: {locale: 'en', }, }, // 165 {}, // 166 {collation: {locale: 'en_US_POSIX', }, }, // 167 {collation: {locale: 'ru', strength: 2, }, }, // 168 {}, // 169 {collation: {locale: 'ko', strength: 5, }, }, // 170 {}, // 171 {collation: {locale: 'ru', strength: 1, }, }, // 172 {}, // 173 {collation: {locale: 'en_US', strength: 1, }, }, // 174 {collation: {locale: 'el', }, }, // 175 {}, // 176 {}, // 177 {collation: {locale: 'el', strength: 5, }, }, // 178 {collation: {locale: 'en_US', strength: 1, }, }, // 179 {}, // 180 {}, // 181 {}, // 182 {}, // 183 {}, // 184 {}, // 185 {collation: {locale: 'en_US', strength: 2, }, }, // 186 {}, // 187 {}, // 188 {}, // 189 {}, // 190 {collation: {locale: 'ru', strength: 3, }, }, // 191 {}, // 192 {}, // 193 {collation: {locale: 'ko', strength: 4, }, }, // 194 {collation: {locale: 'el', strength: 5, }, }, // 195 {}, // 196 {collation: {locale: 'en_US_POSIX', strength: 3, }, }, // 197 {collation: {locale: 'el', strength: 2, }, }, // 198 {collation: {locale: 'en', strength: 4, }, }, // 199 {}, // 200 {}, // 201 {collation: {locale: 'en', strength: 5, }, }, // 202 {}, // 203 {}, // 204 {}, // 205 {collation: {locale: 'en_US_POSIX', strength: 1, }, }, // 206 {collation: {locale: 'en', strength: 3, }, }, // 207 {collation: {locale: 'ru', strength: 5, }, }, // 208 {}, // 209 {}, // 210 {}, // 211 {}, // 212 {collation: {locale: 'ru', strength: 2, }, }, // 213 {}, // 214 {}, // 215 {collation: {locale: 'ko', strength: 1, }, }, // 216 {}, // 217 {}, // 218 {}, // 219 {}, // 220 {collation: {locale: 'en_US', strength: 4, }, }, // 221 {collation: {locale: 'ko', strength: 3, }, }, // 222 {}, // 223 {collation: {locale: 'en_US', strength: 3, }, }, // 224 {}, // 225 {collation: {locale: 'ko', strength: 3, }, }, // 226 {}, // 227 {collation: {locale: 'en', strength: 5, }, }, // 228 {}, // 229 {}, // 230 {}, // 231 {}, // 232 {}, // 233 {}, // 234 {collation: {locale: 'ru', }, }, // 235 {}, // 236 {}, // 237 {collation: {locale: 'el', strength: 4, }, }, // 238 {}, // 239 {collation: {locale: 'el', strength: 3, }, }, // 240 {}, // 241 {}, // 242 {}, // 243 {}, // 244 {collation: {locale: 'en_US', }, }, // 245 {}, // 246 {collation: {locale: 'ru', strength: 1, }, }, // 247 {}, // 248 {}, // 249 ]; const indexList = [ {"measurement0": 1, }, // 0 {"obj.obj.obj.str": 1, }, // 1 {"bool": 1, }, // 2 {"obj.obj.bool": -1, }, // 3 {"obj.obj.obj.str": 1, "tag.assistant": 1, }, // 4 {"obj.obj.bool": -1, "time": 1, }, // 5 {"bool": 1, "obj.str": 1, }, // 6 {"tag": 1, }, // 7 {"tag": 1, "tag.scientist": -1, }, // 8 {"num": -1, }, // 9 {"obj.num": -1, }, // 10 {"measurement0": 1, "bool": 1, }, // 11 {"obj.obj.obj.obj.date": -1, }, // 12 {"measurement4": -1, }, // 13 {"obj.obj.obj.str": -1, }, // 14 {"date": 1, }, // 15 {"obj.obj.obj.bool": 1, }, // 16 {"str": 1, }, // 17 {"measurement4": -1, "bool": -1, }, // 18 {"obj.obj.date": -1, }, // 19 {"obj.bool": 1, }, // 20 {"obj.obj.obj.obj.str": 1, }, // 21 {"tag": -1, }, // 22 {"obj.obj.str": 1, }, // 23 {"measurement1": 1, }, // 24 {"tag.assistant": 1, }, // 25 {"obj.obj.obj.obj.date": 1, }, // 26 {"tag": -1, "time": -1, }, // 27 {"obj.obj.obj.bool": 1, "obj.obj.obj.obj.bool": -1, }, // 28 {"obj.obj.obj.date": 1, }, // 29 {"tag": -1, "obj.obj.date": 1, }, // 30 {"obj.date": 1, }, // 31 {"time": -1, }, // 32 {"obj.obj.num": 1, }, // 33 {"str": 1, "measurement5": 1, }, // 34 {"obj.obj.obj.obj.bool": 1, }, // 35 {"obj.str": 1, }, // 36 {"tag.scientist": -1, }, // 37 {"obj.obj.obj.obj.bool": 1, "obj.obj.obj.obj.date": -1, }, // 38 {"obj.bool": 1, "num": 1, }, // 39 {"obj.obj.str": 1, "measurement3": 1, }, // 40 {"obj.obj.date": -1, "tag": 1, }, // 41 {"tag.scientist": 1, }, // 42 {"tag.scientist": -1, "str": -1, }, // 43 {"obj.date": 1, "obj.obj.obj.obj.str": -1, }, // 44 {"obj.obj.num": 1, "obj.obj.obj.num": 1, }, // 45 {"obj.bool": 1, "measurement7": 1, }, // 46 {"tag.assistant": -1, }, // 47 {"measurement0": 1, "obj.obj.obj.obj.bool": -1, }, // 48 {"tag": 1, "time": -1, }, // 49 {"obj.obj.bool": 1, }, // 50 {"tag.scientist": 1, "num": -1, }, // 51 {"tag.scientist": 1, "obj.str": 1, }, // 52 {"obj.obj.obj.obj.date": 1, "time": -1, }, // 53 {"obj.obj.obj.date": -1, }, // 54 {"tag.assistant": -1, "tag.assistant": -1, }, // 55 {"obj.obj.str": -1, }, // 56 {"obj.obj.obj.date": 1, "tag.scientist": -1, }, // 57 {"tag.scientist": 1, "str": 1, }, // 58 {"bool": -1, }, // 59 {"obj.obj.obj.obj.str": 1, "obj.obj.obj.obj.bool": 1, }, // 60 {"tag": -1, "obj.str": 1, }, // 61 {"obj.bool": 1, "tag.scientist": -1, }, // 62 {"tag.assistant": 1, "num": 1, }, // 63 {"obj.obj.obj.bool": 1, "tag.assistant": 1, }, // 64 {"obj.obj.obj.str": 1, "obj.str": -1, }, // 65 {"obj.date": -1, }, // 66 {"obj.obj.obj.str": 1, "tag": -1, }, // 67 {"obj.obj.obj.str": -1, "obj.date": -1, }, // 68 {"obj.str": -1, }, // 69 {"bool": 1, "obj.obj.str": -1, }, // 70 {"obj.obj.obj.obj.date": -1, "tag.assistant": -1, }, // 71 {"time": 1, }, // 72 {"tag": 1, "obj.num": -1, }, // 73 {"obj.obj.obj.obj.bool": -1, }, // 74 {"obj.obj.obj.obj.str": -1, }, // 75 {"obj.date": 1, "obj.obj.obj.date": -1, }, // 76 {"obj.obj.obj.obj.bool": 1, "measurement0": -1, }, // 77 {"obj.obj.bool": -1, "obj.obj.obj.obj.bool": 1, }, // 78 {"tag": -1, "str": 1, }, // 79 {"time": 1, "obj.obj.obj.obj.date": -1, }, // 80 {"obj.obj.obj.obj.date": 1, "tag": 1, }, // 81 {"tag.scientist": -1, "time": 1, }, // 82 {"bool": -1, "obj.date": 1, }, // 83 {"bool": -1, "obj.bool": -1, }, // 84 {"obj.str": -1, "obj.obj.obj.str": 1, }, // 85 {"tag.assistant": -1, "obj.date": -1, }, // 86 {"obj.obj.bool": -1, "measurement5": -1, }, // 87 {"tag.assistant": 1, "obj.obj.obj.str": 1, }, // 88 {"tag": -1, "tag.assistant": -1, }, // 89 {"obj.obj.str": 1, "tag.scientist": 1, }, // 90 {"obj.obj.bool": 1, "obj.obj.obj.num": -1, }, // 91 {"obj.obj.date": -1, "tag.assistant": -1, }, // 92 {"str": 1, "measurement6": 1, }, // 93 {"measurement4": 1, }, // 94 {"tag.assistant": 1, "time": -1, }, // 95 {"obj.obj.bool": 1, "tag.assistant": 1, }, // 96 {"tag.assistant": 1, "obj.obj.obj.obj.bool": -1, }, // 97 {"tag": 1, "tag.assistant": -1, }, // 98 {"measurement8": 1, }, // 99 {"obj.date": -1, "time": -1, }, // 100 {"tag.assistant": 1, "obj.obj.obj.obj.bool": 1, }, // 101 {"tag.scientist": -1, "obj.bool": 1, }, // 102 {"time": -1, "obj.obj.obj.str": 1, }, // 103 {"tag.assistant": -1, "bool": -1, }, // 104 {"obj.obj.date": 1, }, // 105 {"tag.scientist": -1, "obj.obj.date": 1, }, // 106 {"str": -1, }, // 107 {"obj.obj.obj.obj.bool": 1, "obj.obj.obj.date": 1, }, // 108 {"tag.assistant": 1, "obj.num": 1, }, // 109 {"tag.scientist": -1, "str": -1, "obj.obj.obj.obj.bool": -1, }, // 110 {"obj.obj.obj.obj.date": 1, "measurement4": 1, }, // 111 {"obj.str": 1, "obj.bool": -1, }, // 112 {"time": -1, "tag.scientist": 1, }, // 113 {"tag": -1, "measurement5": 1, }, // 114 {"obj.obj.obj.obj.bool": 1, "tag": 1, }, // 115 {"time": 1, "str": 1, }, // 116 {"tag": 1, "tag.scientist": 1, }, // 117 {"measurement8": -1, }, // 118 {"obj.str": 1, "tag.assistant": 1, }, // 119 {"tag.assistant": -1, "str": 1, }, // 120 {"obj.obj.str": -1, "obj.str": -1, }, // 121 {"obj.str": -1, "obj.num": -1, }, // 122 {"obj.obj.obj.obj.str": 1, "obj.obj.str": 1, }, // 123 {"measurement4": -1, "obj.obj.bool": -1, }, // 124 {"obj.obj.bool": 1, "bool": -1, }, // 125 {"tag.scientist": 1, "obj.obj.date": -1, }, // 126 {"str": 1, "obj.obj.obj.str": 1, }, // 127 {"obj.str": -1, "measurement8": 1, }, // 128 {"obj.date": -1, "tag.assistant": 1, }, // 129 {"bool": -1, "date": 1, }, // 130 {"obj.obj.obj.str": -1, "obj.obj.num": -1, }, // 131 {"obj.obj.obj.date": -1, "measurement6": -1, }, // 132 {"str": 1, "measurement0": -1, }, // 133 {"date": 1, "tag.scientist": -1, }, // 134 {"obj.obj.date": 1, "tag": 1, }, // 135 {"time": 1, "obj.obj.obj.obj.bool": -1, }, // 136 {"tag.scientist": 1, "bool": -1, }, // 137 {"time": -1, "obj.bool": -1, }, // 138 {"obj.str": -1, "obj.obj.obj.str": 1, "date": 1, }, // 139 {"obj.obj.obj.obj.bool": -1, "obj.obj.str": -1, }, // 140 {"obj.obj.obj.str": -1, "tag.scientist": -1, }, // 141 {"time": 1, "tag.scientist": 1, }, // 142 {"obj.obj.num": 1, "obj.obj.date": -1, }, // 143 {"obj.obj.obj.str": 1, "obj.obj.obj.date": 1, }, // 144 {"tag.scientist": 1, "measurement7": 1, }, // 145 {"obj.obj.str": -1, "obj.obj.obj.obj.bool": 1, }, // 146 {"obj.bool": 1, "tag.scientist": 1, }, // 147 {"tag.scientist": 1, "obj.obj.obj.obj.bool": -1, }, // 148 {"tag": 1, "obj.obj.str": -1, }, // 149 {"tag": -1, "obj.obj.obj.obj.bool": -1, }, // 150 {"obj.obj.obj.obj.date": -1, "bool": 1, }, // 151 {"obj.date": -1, "tag.scientist": 1, }, // 152 {"measurement4": -1, "obj.obj.obj.obj.str": 1, }, // 153 {"measurement0": 1, "tag.assistant": -1, }, // 154 {"measurement1": 1, "time": 1, }, // 155 {"measurement1": -1, }, // 156 {"obj.obj.date": -1, "obj.obj.obj.bool": 1, }, // 157 {"obj.str": 1, "measurement7": 1, }, // 158 {"tag.assistant": 1, "obj.str": -1, }, // 159 {"tag": 1, "obj.obj.str": -1, "measurement0": 1, }, // 160 {"tag.scientist": -1, "tag": 1, }, // 161 {"obj.obj.obj.str": 1, "obj.obj.obj.obj.bool": -1, }, // 162 {"date": -1, }, // 163 {"obj.obj.obj.obj.str": -1, "tag": 1, }, // 164 {"obj.obj.obj.str": 1, "obj.obj.obj.obj.bool": -1, "obj.obj.str": 1, }, // 165 {"obj.date": 1, "obj.obj.obj.obj.date": 1, }, // 166 {"obj.obj.obj.num": 1, }, // 167 {"obj.obj.obj.obj.bool": 1, "obj.num": -1, }, // 168 {"obj.obj.obj.obj.date": 1, "obj.obj.date": -1, }, // 169 {"obj.obj.obj.obj.num": -1, }, // 170 {"obj.obj.obj.bool": -1, }, // 171 {"bool": 1, "obj.bool": 1, }, // 172 {"date": -1, "obj.obj.bool": -1, }, // 173 {"tag.assistant": 1, "tag": 1, }, // 174 {"measurement1": -1, "obj.obj.obj.bool": 1, }, // 175 {"obj.obj.obj.obj.num": 1, }, // 176 {"tag.scientist": -1, "tag": 1, "tag.assistant": -1, }, // 177 {"tag.scientist": 1, "obj.obj.str": -1, }, // 178 {"tag.scientist": 1, "tag": 1, }, // 179 {"obj.obj.num": -1, }, // 180 {"measurement5": 1, }, // 181 {"tag.assistant": -1, "obj.obj.obj.bool": 1, }, // 182 {"obj.obj.obj.obj.str": 1, "obj.obj.date": 1, }, // 183 {"measurement7": -1, }, // 184 {"measurement7": 1, }, // 185 {"obj.str": -1, "date": 1, }, // 186 {"tag.assistant": 1, "time": 1, }, // 187 {"measurement6": -1, }, // 188 {"obj.obj.bool": -1, "bool": -1, }, // 189 {"num": -1, "obj.obj.obj.date": 1, }, // 190 {"obj.obj.num": -1, "bool": -1, }, // 191 {"obj.obj.obj.obj.date": -1, "measurement4": -1, }, // 192 {"measurement3": 1, }, // 193 {"obj.obj.obj.obj.str": -1, "tag.assistant": 1, }, // 194 {"measurement0": -1, }, // 195 {"time": -1, "obj.obj.obj.date": -1, }, // 196 {"obj.obj.obj.date": -1, "obj.obj.str": 1, }, // 197 {"tag": 1, "str": 1, }, // 198 {"obj.obj.bool": 1, "measurement7": -1, }, // 199 {"tag.assistant": 1, "measurement6": -1, }, // 200 {"obj.obj.obj.obj.bool": 1, "measurement7": 1, }, // 201 {"tag": -1, "obj.obj.obj.obj.bool": 1, }, // 202 {"obj.num": 1, }, // 203 {"obj.obj.obj.date": 1, "obj.obj.obj.obj.bool": -1, }, // 204 {"obj.obj.obj.bool": 1, "measurement8": -1, }, // 205 {"obj.obj.bool": -1, "obj.date": 1, }, // 206 {"time": 1, "tag.assistant": -1, }, // 207 {"obj.bool": -1, }, // 208 {"obj.obj.str": 1, "bool": -1, }, // 209 {"str": -1, "obj.obj.obj.date": 1, }, // 210 {"obj.obj.obj.obj.date": -1, "obj.obj.obj.obj.str": -1, }, // 211 {"obj.bool": 1, "obj.obj.obj.obj.date": -1, }, // 212 {"num": 1, }, // 213 {"measurement3": 1, "obj.obj.obj.obj.date": -1, }, // 214 {"tag": 1, "date": -1, }, // 215 {"obj.num": 1, "tag": 1, }, // 216 {"tag.assistant": 1, "tag.assistant": -1, }, // 217 {"obj.obj.obj.str": -1, "obj.obj.str": 1, }, // 218 {"tag.scientist": 1, "obj.obj.str": -1, "obj.obj.obj.str": -1, }, // 219 {"tag": -1, "obj.obj.str": -1, }, // 220 {"obj.obj.obj.obj.str": 1, "tag.scientist": 1, }, // 221 {"obj.obj.obj.obj.bool": 1, "tag.scientist": -1, }, // 222 {"tag.assistant": -1, "measurement1": 1, }, // 223 {"obj.obj.date": 1, "tag": 1, "tag.scientist": -1, }, // 224 {"measurement1": -1, "tag.scientist": 1, }, // 225 {"tag.assistant": 1, "obj.bool": 1, }, // 226 {"tag": 1, "obj.obj.str": -1, "obj.obj.bool": 1, }, // 227 {"str": -1, "tag.assistant": 1, }, // 228 {"obj.obj.num": 1, "date": 1, }, // 229 {"measurement6": 1, }, // 230 {"obj.str": -1, "obj.obj.str": -1, }, // 231 {"obj.obj.bool": 1, "obj.obj.obj.obj.date": 1, }, // 232 {"obj.obj.str": -1, "num": 1, }, // 233 {"obj.date": -1, "tag": 1, }, // 234 {"tag.assistant": -1, "measurement7": -1, }, // 235 {"obj.obj.str": -1, "obj.obj.obj.date": 1, }, // 236 {"obj.obj.bool": -1, "obj.obj.obj.obj.bool": -1, }, // 237 {"tag": 1, "obj.obj.obj.obj.bool": -1, }, // 238 {"obj.obj.obj.bool": -1, "obj.obj.obj.str": -1, }, // 239 {"obj.str": -1, "tag.scientist": -1, }, // 240 {"obj.str": 1, "str": -1, }, // 241 {"obj.obj.str": -1, "num": 1, "obj.obj.obj.obj.bool": -1, }, // 242 {"obj.obj.obj.bool": -1, "obj.obj.obj.obj.str": -1, }, // 243 {"tag.assistant": -1, "obj.date": -1, "obj.obj.num": 1, }, // 244 {"measurement8": 1, "obj.obj.obj.bool": 1, }, // 245 {"tag.scientist": -1, "obj.num": 1, }, // 246 {"tag.assistant": -1, "obj.obj.obj.date": -1, }, // 247 {"obj.obj.obj.bool": 1, "measurement6": 1, }, // 248 {"obj.bool": -1, "date": 1, }, // 249 ]; const optimizationFailPointList = [ 'disableMatchExpressionOptimization', 'disablePipelineOptimization', ]; const latestVersion = {major: 0, minor: 0}; const useEsModules = true; const aggGenerated = /*#__PURE__*/Object.freeze({ __proto__: null, aggregationList: aggregationList, aggregationOptionsList: aggregationOptionsList, collectionNames: collectionNames, debug: debug, diffTestingMode: diffTestingMode, documentList: documentList, fuzzerName: fuzzerName, indexOptionList: indexOptionList, indexList: indexList, optimizationFailPointList: optimizationFailPointList, latestVersion: latestVersion, useEsModules: useEsModules }); const shell = globalThis; function tempDir() { return shell._getEnv('TMPDIR') || '/tmp'; } function checkpointsDir() { return tempDir().concat('/mongodb-jstestfuzz-checkpoints/'); } function ensureTempDir() { try { shell.mkdir(tempDir()); } catch (err) { // pass } try { shell.mkdir(checkpointsDir()); } catch (err) { // pass } } /* Encapsulates methods associated with starting and stopping * the fuzzer */ class MongoProcessManager { // Set up a standalone mongod of a given version. startMongod({ version: version = 'latest', setParameter: setParameter = {} } = {}) { const conn = shell.MongoRunner.runMongod({ binVersion: version, setParameter }); if (!conn) { throw new Error('Failed to start mongod with binVersion=' + version); } return conn; } // Set up a replica set of a given version. 'diffTestingMode' is used only to name the replica set. startReplTest({ version: version = 'latest', diffTestingMode: diffTestingMode = 'none', setParameter: setParameter = {}, serverless: serverless = false, } = {}) { const newSetParameter = Object.assign({ periodicNoopIntervalSecs: 1, writePeriodicNoops: true }, setParameter); const replOptions = { nodes: 1, nodeOptions: { binVersion: version, setParameter: newSetParameter, }, name: `rst_diff_mode_${diffTestingMode}`, }; if (serverless) { replOptions['serverless'] = serverless; } const rst = new shell.ReplSetTest(replOptions); rst.startSet(); rst.initiate(); return rst; } startShardedTest({ setParameter: setParameter = {} }) { return new shell.ShardingTest({ mongos: 1, config: 1, shards: 2, rs: { nodes: 1 }, other: { shardOptions: { setParameter }, configOptions: { setParameter }, rsOptions: { setParameter }, }, }); } enableFailpoint({ node, failpoint }) { shell.assert.commandWorked(node.adminCommand({ configureFailPoint: failpoint, mode: 'alwaysOn', })); } disableFailpoint({ node, failpoint }) { shell.assert.commandWorked(node.adminCommand({ configureFailPoint: failpoint, mode: 'off', })); } disableOptimizationViaFailpoints({ server, optimizationFailPointList, }) { for (const fp of optimizationFailPointList) { if (server instanceof shell.ReplSetTest) { server.nodes.forEach(conn => { this.enableFailpoint({ node: conn, failpoint: fp, }); }); } else { this.enableFailpoint({ node: server, failpoint: fp, }); } } return server; } enableOptimizationViaFailpoints({ node, optimizationFailPointList, }) { for (const fp of optimizationFailPointList) { this.disableFailpoint({ node, failpoint: fp }); } return node; } setupFixture({ diffTestingMode, optimizationFailPointList, useReplSet = false, }) { const exp = this.setUpServer({ diffTestingMode: 'none', experimentDiffTestingMode: diffTestingMode, optimizationFailPointList, useReplSet, }); const control = this.setUpServer({ diffTestingMode, experimentDiffTestingMode: diffTestingMode, optimizationFailPointList, useReplSet, }); return { servers: [exp.server, control.server], dbs: [exp.db, control.db], }; } /** * @param diffTestingMode * @param experimentDiffTestingMode * @param optimizationFailPointList * @param useReplSet */ setUpServer({ diffTestingMode, experimentDiffTestingMode, optimizationFailPointList, useReplSet = false, }) { // We deprecated the use of 'last-stable' in versions of the shell greater than '4.4'. // MongoRunner.compareBinVersions is only supported in versions of the shell greater than '3.6'. const supportsCompareBinVersions = typeof shell.MongoRunner.compareBinVersions === 'function'; // `useRandomBinVersionsWithinReplicaSet` is a boolean on `5.1` and earlier branches // and there is no need to update it since `last-lts` and `last-continuous` are the same if (shell.TestData.useRandomBinVersionsWithinReplicaSet === true) { shell.TestData.useRandomBinVersionsWithinReplicaSet = 'last-lts'; } const oldVersion = supportsCompareBinVersions && shell.MongoRunner.compareBinVersions('latest', '4.4') === 1 ? shell.TestData.useRandomBinVersionsWithinReplicaSet || 'last-lts' : 'last-stable'; const defaultSetParameter = {}; // setWindowFields can have non-deterministic output by default, setting this query knob ensures // that _id is always appended to the sortBy to ensure that the output // is sorted deterministically. const supportsSetWindowFields = (shell.TestData.internalQueryAppendIdToSetWindowFieldsSort === true && diffTestingMode !== 'version') || (diffTestingMode === 'version' && supportsCompareBinVersions && shell.MongoRunner.compareBinVersions(oldVersion, '5.0') >= 0); if (supportsSetWindowFields) { // Can't just set the knob directly since it doesn't exist in versions under 5.0. defaultSetParameter['internalQueryAppendIdToSetWindowFieldsSort'] = true; } // Enable Query Shape Stats for the optimization fuzzer. We are excluding multiversion so we // can ensure a 6.0+ version with all queries are being reported on. Note that failures in // $queryStats are process fatal in order to make as much noise as possible to help us // identify bugs in testing runs. if (diffTestingMode === 'optimization' && shell.MongoRunner.compareBinVersions('latest', '6.0') >= 0) { // Set Query Stats Rate Limit to unlimited with -1. defaultSetParameter['internalQueryStatsRateLimit'] = -1; defaultSetParameter['internalQueryStatsErrorsAreCommandFatal'] = true; } // Disable the plan caching in experiment. if (diffTestingMode === 'none' && experimentDiffTestingMode === 'plan_cache') { defaultSetParameter['internalQueryDisablePlanCache'] = true; } // Override column store index query planning heuristic in experiment so // that we will use column store indexes whenever possible. if (diffTestingMode === 'none' && experimentDiffTestingMode === 'columnstore') { defaultSetParameter['internalQueryColumnScanMinAvgDocSizeBytes'] = 0; } const mongoServer = (() => { switch (diffTestingMode) { case 'version': return this.startMongod({ setParameter: defaultSetParameter, version: oldVersion, }); case 'optimization': const optOptions = { setParameter: defaultSetParameter, diffTestingMode }; const server = useReplSet ? this.startReplTest(optOptions) : this.startMongod(optOptions); return this.disableOptimizationViaFailpoints({ server, optimizationFailPointList, }); case 'timeseries': return this.disableOptimizationViaFailpoints({ server: this.startMongod({ setParameter: defaultSetParameter, }), optimizationFailPointList, }); case 'blockprocessing': defaultSetParameter['internalQueryFrameworkControl'] = 'trySbeEngine'; defaultSetParameter['featureFlagSbeFull'] = true; return this.startMongod({ setParameter: defaultSetParameter, }); case 'serverless': case 'serverlessNoOptimization': shell.assert(useReplSet); // Add the required feature flags to enable serverless mode. defaultSetParameter['featureFlagServerlessChangeStreams'] = true; defaultSetParameter['multitenancySupport'] = true; defaultSetParameter['testOnlyValidatedTenancyScopeKey'] = 'secret'; defaultSetParameter['featureFlagRequireTenantID'] = true; // 'featureFlagMongoStore' was renamed in 6.3. if (shell.MongoRunner.compareBinVersions(oldVersion, '6.3') < 0) { defaultSetParameter['featureFlagMongoStore'] = true; } else { defaultSetParameter['featureFlagSecurityToken'] = true; } const replOptions = { setParameter: defaultSetParameter, serverless: true, diffTestingMode, }; // For diff testing mode 'serverless', return the replica set with change // stream optimization enabled. { return this.startReplTest(replOptions); } case 'sharded': return this.startShardedTest({ setParameter: defaultSetParameter }); case 'bonsai_m2_vs_sbe_stagebuilders': case 'bonsai_m2_vs_classic': case 'standalone': case 'plan_cache': case 'wildcard': case 'columnstore': case 'none': default: { const options = { setParameter: defaultSetParameter, diffTestingMode }; return useReplSet ? this.startReplTest(options) : this.startMongod(options); } } })(); if (diffTestingMode === 'sharded') { shell.assert.commandWorked(mongoServer.s0.adminCommand({ enableSharding: 'fuzzer', })); } // In the Bonsai fuzzers, set the frameworkControl knob at runtime so that it // takes precedence over any default set by the build variant. This applies to // both the query and the agg fuzzers, since they share the same diffTestingModes. if (experimentDiffTestingMode === 'bonsai_m2_vs_sbe_stagebuilders' || experimentDiffTestingMode === 'bonsai_m2_vs_classic') { shell.assert(!useReplSet, 'Not implemented: Bonsai fuzzer with replication. ' + 'We will need to make sure all replicas use the appropriate knob.'); if (diffTestingMode === 'none') { shell.assert.commandWorked(mongoServer.adminCommand({ setParameter: 1, internalQueryFrameworkControl: 'tryBonsai', })); // Currently, we need to enable this failpoint to ensure explain commands are eligible // for Bonsai. This ensures that when the fuzzers find a result set difference and dump // explains to the logs, we see CQF explain output. TODO: SERVER-77719 remove this. shell.assert.commandWorked(mongoServer.adminCommand({ configureFailPoint: 'enableExplainInBonsai', mode: 'alwaysOn', })); } else if (diffTestingMode === 'bonsai_m2_vs_sbe_stagebuilders') { shell.assert.commandWorked(mongoServer.adminCommand({ setParameter: 1, internalQueryFrameworkControl: 'trySbeEngine', })); } else if (diffTestingMode === 'bonsai_m2_vs_classic') { shell.assert.commandWorked(mongoServer.adminCommand({ setParameter: 1, internalQueryFrameworkControl: 'forceClassicEngine', })); } else { shell.assert(false, `Unexpected diffTestingMode=${diffTestingMode} given experimentDiffTestingMode=${experimentDiffTestingMode}`); } } if (mongoServer instanceof shell.ReplSetTest) { // For the serverless, the 'fuzzerDb' must be retrieved using the tenant's connection // to the primary. const serverless = diffTestingMode === 'serverless' || diffTestingMode === 'serverlessNoOptimization'; const fuzzerDb = !serverless ? mongoServer.getPrimary().getDB('fuzzer') : this.getTenantConnection(mongoServer).getDB('fuzzer'); return { server: mongoServer, db: fuzzerDb }; } else { return { server: mongoServer, db: mongoServer.getDB('fuzzer') }; } } // Returns a connection to the replica set primary that is stamped with a tenant id. This // method must only be called when 'multitenancySupport' flag is enabled. getTenantConnection(rst) { // Create a 'root' user so that '$tenant' can be used with commands. shell.assert.commandWorked(rst .getPrimary() .getDB('admin') .runCommand({ createUser: 'root', pwd: 'pwd', roles: ['root'] })); // Create a tenant and a user for testing purposes. const tenantId = shell.ObjectId(); const user = 'csServerlessFuzzer'; const hostAddr = rst.getPrimary().host; const conn = new shell.Mongo(hostAddr); const authDBName = 'admin'; const adminDB = conn.getDB(authDBName); const external = conn.getDB('$external'); // Authenticate with the 'root' user on the replica set primary and create a user with // required privileges. shell.assert(adminDB.auth('root', 'pwd')); const createUserCmd = { createUser: user, roles: [{ role: 'readWriteAnyDatabase', db: authDBName }], }; const unsignedToken = shell._createTenantToken({ tenant: tenantId }); shell.assert.commandWorked(this.runCommandWithSecurityToken(unsignedToken, external, createUserCmd)); // Associate the user with the tenant id and then logout from 'root'. conn._setSecurityToken(shell._createSecurityToken({ user, db: '$external', tenant: tenantId }, 'secret')); adminDB.logout(); // Return the connection with stamped tenant id. return conn; } tearDownFixture(dbs, mongoServers, diffTestingMode) { // The 'profile' command is not available for direct use in the serverless. if (diffTestingMode !== 'serverless' && diffTestingMode !== 'serverlessNoOptimization') { this.disableDBProfiling(dbs, mongoServers); } shell.jsTest.log('Done, stopping mongo server instances'); this.stopServers(mongoServers); } stopServers(mongoServers, forRestart = false) { mongoServers.forEach(mongoServer => { if (mongoServer instanceof shell.ShardingTest) { if (forRestart) { const mongosOpts = { restart: true }; mongoServer.stopAllMongos(mongosOpts); const opts = { ...mongosOpts, noCleanData: true, forRestart: true }; mongoServer.stopAllShards(opts, true); mongoServer.stopAllConfigServers(opts, true /*forRestart*/); } else { mongoServer.stop(); } } else if (mongoServer instanceof shell.ReplSetTest) { mongoServer.stopSet(undefined, forRestart); } else { shell.MongoRunner.stopMongod(mongoServer); } }); } restartServers(mongoServers) { const newServers = []; mongoServers.forEach(mongoServer => { if (mongoServer instanceof shell.ShardingTest) { const mongosOpts = { restart: true }; const opts = { ...mongosOpts, noCleanData: true, forRestart: true }; mongoServer.restartAllConfigServers(opts); mongoServer.restartAllShards(opts); mongoServer.restartAllMongos(mongosOpts); } else if (mongoServer instanceof shell.ReplSetTest) { mongoServer = mongoServer.startSet(undefined /*options*/, true /*restart*/); } else { mongoServer = shell.MongoRunner.runMongod({ cleanData: false, startClean: false, restart: true, }); } newServers.push(mongoServer); }); // When there is more than one server, we return an object of the form // { servers: shell.Server[], dbs: shell.DB[] }, otherwise, return // { server: shell.Server, db: shell.DB } if (newServers.length === 1) { const mongoServer = newServers[0]; if (mongoServer instanceof shell.ReplSetTest) { return { server: mongoServer, db: mongoServer.getPrimary().getDB('fuzzer') }; } else { return { server: mongoServer, db: mongoServer.getDB('fuzzer') }; } } else { return { servers: newServers, dbs: newServers.map((mongoServer) => { if (mongoServer instanceof shell.ReplSetTest) { return mongoServer.getPrimary().getDB('fuzzer'); } else { return mongoServer.getDB('fuzzer'); } }), }; } } /** * One form of blacklisting supported by the query fuzzers is by query_plan type. * This function enables db profiling for all non-mongos connections, which will * gather statistics on all future commands executed. * @param dbs database instances */ enableDBProfiling(dbs, mongoServers) { for (let i = 0; i < dbs.length; i++) { // Cannot set profiling level through a mongos. if (!(mongoServers[i] instanceof shell.ShardingTest)) { dbs[i].setProfilingLevel(2); } } } /** * Disables profiling and deletes system.profile collection for all non-mongos connections. * @param dbs database instances */ disableDBProfiling(dbs, mongoServers) { for (let i = 0; i < dbs.length; i++) { // Cannot set profiling level through a mongos. if (!(mongoServers[i] instanceof shell.ShardingTest)) { dbs[i].setProfilingLevel(0); } } } /** * Prints out the distribution of documents amongst shards and chunks. * @param db database instance * @param collections names of all collections */ getShardDistribution(db, collections) { for (const coll of collections) { db.adminCommand({ flushRouterConfig: `fuzzer.${coll}` }); shell.print('Shard distribution for: ' + `fuzzer.${coll}`); db.getSiblingDB('fuzzer')[coll].getShardDistribution(); } } clearCheckpoints() { shell.removeFile(checkpointsDir()); } // Make a checkpoint of the given server by shutting it down and copying // its datafiles to another location. Restarts the server using setUpServer, // and returns a new shell.Server and shell.DB. makeCheckpoints(servers) { ensureTempDir(); const [dbPaths, checkpointPaths] = this._collect_checkpoint_paths(servers); this.stopServers(servers, true); shell.resetAllocatedPorts(); dbPaths.forEach((dbPath, i) => { shell.print('Creating minimizer checkpoint for ', dbPath, 'at', checkpointPaths[i]); shell.copyDir(dbPath, checkpointPaths[i]); shell.print('Created minimizer checkpoint for ', dbPath, 'at', checkpointPaths[i]); }); return this.restartServers(servers); } _collect_checkpoint_paths(servers) { const dbPaths = []; const checkpointPaths = []; ensureTempDir(); for (let i = 0; i < servers.length; i++) { const mongoServer = servers[i]; if (mongoServer instanceof shell.ShardingTest) { mongoServer._rs.forEach(rs => { rs.nodes.forEach(node => { dbPaths.push(node.dbpath); checkpointPaths.push(checkpointsDir().concat(node.port)); }); }); } else if (mongoServer instanceof shell.ReplSetTest) { mongoServer.nodes.forEach(node => { dbPaths.push(node.dbpath); checkpointPaths.push(checkpointsDir().concat(node.port)); }); } else { dbPaths.push(servers[i].dbpath); checkpointPaths.push(checkpointsDir().concat(servers[i].port)); } } shell.assert(dbPaths.length === checkpointPaths.length); return [dbPaths, checkpointPaths]; } runCommandWithSecurityToken(tenantToken, db, command) { const conn = db.getMongo(); const preToken = conn._securityToken; try { conn._setSecurityToken(tenantToken); return db.runCommand(command); } finally { conn._setSecurityToken(preToken); } } restoreCheckpoints(servers) { const [dbPaths, checkpointDirs] = this._collect_checkpoint_paths(servers); this.stopServers(servers, true); shell.resetAllocatedPorts(); dbPaths.forEach((dbPath, i) => { shell.print('Restoring minimizer checkpoint for ', dbPath, 'from', checkpointsDir[i]); shell.removeFile(dbPath); shell.copyDir(checkpointDirs[i], dbPath); shell.print('Restored minimizer checkpoint for ', dbPath, 'from', checkpointsDir[i]); }); return this.restartServers(servers); } } const mongoProcessManager = new MongoProcessManager(); const shell$1 = globalThis; /** * Encapsulates list of parameters used to create list of indexes. */ class CreateIndexParameters { constructor() { this.dbs = []; this.collections = []; this.indexes = []; this.indexOptions = []; this.desiredIndexes = 0; this.additionalErrorCodes = []; } } /* Encapsulates methods associated with seeding the fuzzer * data */ class SeedData { actOnAllCollections(dbs, colls, action) { dbs.forEach(db => { colls.forEach(coll => { action(db[coll]); }); }); } /* Takes an array of dbs and collections in addition to an action which * returns true if the action succeeded and returns false if the operation * failed. The final parameter is a function undoAction which undoes action * under the assumption that it succeeded. It is expected that undoAction will * throw an exception if it's unable to undo the action. This function will * attempt to perform action on all db collection pairs, and if it fails on a * single db collection pair it will undo the action on all succeeding db * collection pairs. It returns true if the action succeeded on all db * collection pairs, and false otherwise. */ actUniformlyOnAllCollections(dbs, colls, action, undoAction) { /* Populate results array. */ const results = new Array(dbs.length); for (let i = 0; i < dbs.length; ++i) { results[i] = new Array(colls.length).fill(false); } /* Attempt to act on each db collection pair. Break if at least one fails. * Store results in the results array. */ let atLeastOneFalse = false; for (let i = 0; i < dbs.length; ++i) { for (let j = 0; j < colls.length; ++j) { if (!atLeastOneFalse) { results[i][j] = action(dbs[i][colls[j]]); atLeastOneFalse = atLeastOneFalse || !results[i][j]; } if (atLeastOneFalse) { break; } } if (atLeastOneFalse) { break; } } if (atLeastOneFalse) { /* We can "unact" if we undo the action on all pairs wher it succeeded. */ for (let i = 0; i < dbs.length; ++i) { for (let j = 0; j < colls.length; ++j) { if (results[i][j]) { undoAction(dbs[i][colls[j]]); } } } } /* We return whether all actions succeeded. */ return !atLeastOneFalse; } dropCollections({ dbs, collections }) { this.actOnAllCollections(dbs, collections, coll => coll.drop()); } createTimeseriesCollections(dbs, collections) { const timeseriesParams = { timeField: 'time', metaField: 'tag' }; const canUseFixedBucketingParameters = shell$1.MongoRunner.compareBinVersions(shell$1.version(), '6.3') >= 0; const makeFixedBucketCollection = canUseFixedBucketingParameters ? Math.floor(Math.random() * 10) < 3 : 0; // Results in aggregation pipelines can differ if the collections are made with different bucketing parameters. // Therefore, all collections for each run must be created with the same semi-randomly generated bucketing parameters. if (makeFixedBucketCollection) { // Generate a random integer between 1 and 31536000 (all possible values for bucketMaxSpanSeconds). const span = Math.floor(Math.random() * 31536000) + 1; timeseriesParams['bucketMaxSpanSeconds'] = span; timeseriesParams['bucketRoundingSeconds'] = span; } else { // Randomly choose a granularity. const granularity = ['seconds', 'minutes', 'hours']; const index = Math.floor(Math.random() * granularity.length); timeseriesParams['granularity'] = granularity[index]; } dbs.forEach(db => { collections.forEach(coll => { shell$1.assert.commandWorked(db.createCollection(coll, { timeseries: timeseriesParams })); }); }); } insertDocs(dbs, collections, docs) { this.actOnAllCollections(dbs, collections, coll => { shell$1.assert.commandWorked(coll.insertMany(docs)); }); } removeDocs(dbs, collections, docs) { this.actOnAllCollections(dbs, collections, coll => { docs.forEach(doc => shell$1.assert.commandWorked(coll.deleteOne(doc))); }); } runCommands(dbs, cmds) { dbs.forEach(db => { cmds.forEach(cmd => { shell$1.assert.commandWorked(db.runCommand(cmd)); }); }); } createIndex({ dbs, collections, index, indexOption, }) { this.actOnAllCollections(dbs, collections, coll => { shell$1.assert.commandWorked(coll.createIndex(index, indexOption)); }); } indexNameIndexKeyPattern(keyPattern) { return Object.getOwnPropertyNames(keyPattern) .map(fieldName => { return `${fieldName}_${keyPattern[fieldName]}`; }) .join('_'); } dropIndex(dbs, collections, index, indexOptions) { // There must always be a corresponding index option. It could be empy, // but it must exist. shell$1.assert(indexOptions !== undefined); const explicitIndexName = indexOptions.name !== undefined && indexOptions.name !== null ? indexOptions.name : null; const indexName = explicitIndexName !== null ? explicitIndexName : this.indexNameIndexKeyPattern(index); this.actOnAllCollections(dbs, collections, coll => { const logMsg = { dropIndex: { IndexArg: index, IndexOptionsArg: indexOptions === undefined ? 'undefined' : indexOptions, explicitIndexName, indexName, ActualIndexes: coll.getIndexes(), }, }; shell$1.assert.commandWorked(coll.dropIndex(indexName), JSON.stringify(logMsg)); }); } createDesiredNumIndexes({ dbs, collections, indexes, indexOptions, desiredIndexes, additionalErrorCodes, }) { const blacklistedErrorCodes = [ 11000, 17280, 72, 86, 171, 5930501, ].concat(additionalErrorCodes); const out = { indexes: [], options: [] }; let numIndexesCreated = 0; for (let i = 0; i < indexes.length; i++) { const result = this.actUniformlyOnAllCollections(dbs, collections, coll => { const res = shell$1.assert.commandWorkedOrFailedWithCode(coll.createIndex(indexes[i], indexOptions[i]), blacklistedErrorCodes); return res.ok; }, coll => { shell$1.assert.commandWorked(coll.dropIndex(indexes[i], indexOptions[i])); }); if (result) { out.indexes.push(indexes[i]); out.options.push(indexOptions[i]); numIndexesCreated++; } if (numIndexesCreated === desiredIndexes) { break; } } shell$1.assert.eq(numIndexesCreated, desiredIndexes, `number of created indexes should equal desired number ${desiredIndexes}`); return out; } createIndexes({ dbs, collections, indexes, indexOptions, diffTestingMode, }) { // TODO: SERVER-58834 Change '5.1' to '5.2' once we update the FCV // constants. if ((diffTestingMode === 'timeseries' || diffTestingMode === 'blockprocessing') && shell$1.MongoRunner.compareBinVersions(shell$1.version(), '5.1') < 0) { const out = { indexes: [], options: [] }; this.actOnAllCollections(dbs, collections, coll => { // We create all indexes on meta fields, time fields and all // compound indexes on pairs of these fields. const metaIndexes = ['tag.scientist', 'tag.assistant', 'time']; metaIndexes.forEach(index => { // Create indexes for each meta/time field. const metaIndex = { [index]: 1 }; shell$1.assert.commandWorked(coll.createIndex(metaIndex, {})); out.indexes.push(metaIndex); out.options.push({}); // Create all compound indexes on two fields with the first field // being 'index'. metaIndexes.forEach(index2 => { const compoundIndex = { [index]: 1, [index2]: 1 }; shell$1.assert.commandWorked(coll.createIndex(compoundIndex)); out.indexes.push(compoundIndex); out.options.push({}); }); }); }); return out; } if (diffTestingMode === 'wildcard') { const dbExperiment = dbs[0]; const canCreateCompoundWildcardIndexes = shell$1.MongoRunner.compareBinVersions(shell$1.version(), '7.0') >= 0; // Note: we only want to create indexes on the experiment database. const indexCreationInput = new CreateIndexParameters(); indexCreationInput.dbs = [dbExperiment]; indexCreationInput.collections = collections; if (canCreateCompoundWildcardIndexes) { indexCreationInput.indexes = indexes; indexCreationInput.indexOptions = indexOptions; indexCreationInput.desiredIndexes = 63; // Max number of indexes - 1 for _id. indexCreationInput.additionalErrorCodes = [ // Grammar may generate indexes with overlapping wildcard & // index field paths. 7246204, // Non-wildcard fields may not be multikey. 7246301, ]; } else { // We only want to create one index: the wildcard index. indexCreationInput.indexes = [{ '$**': 1 }]; indexCreationInput.indexOptions = [{}]; indexCreationInput.desiredIndexes = 1; } return this.createDesiredNumIndexes(indexCreationInput); } else if (diffTestingMode === 'columnstore' && shell$1.MongoRunner.compareBinVersions(shell$1.version(), '6.1') >= 0) { const dbExperiment = dbs[0]; const theIndex = { '$**': 'columnstore' }; this.actOnAllCollections([dbExperiment], collections, coll => { shell$1.assert.commandWorked(coll.createIndex(theIndex, {})); }); return { indexes: [theIndex], options: [{}] }; } // Since a collection is only allowed to have 64 indexes including the // default _id index, we create 63 additional indexes to maximize the // number of different query plans we might execute based on the input // documents. let desiredIndexes = 63; // We create 1 less due to shardKey for 'sharded' case. if (diffTestingMode === 'sharded') { desiredIndexes--; } // Don't expect more indexes to be created than the number we attempt to create, // in case we only attempt to create fewer than 63. desiredIndexes = Math.min(desiredIndexes, indexes.length); return this.createDesiredNumIndexes({ dbs, collections, indexes, indexOptions, desiredIndexes, additionalErrorCodes: [], }); } } const seedData = new SeedData(); /** * Helper class to implement generic minimization logic that works by trying to remove some item from an observable, * then testing the result by calling a test function ('cmdFn') below. * * 'minimize()' determines if the item at the given index is essential based on whether or not 'cmdFn' returns the expected error, and then outputs a list of essential elements. */ class Minimizable { constructor(length) { this.length = length; this.essentialIndexes = []; } remove(idx) { this._remove(idx); } replace(idx) { this.essentialIndexes.push(idx); this._replace(idx); } /** * Generic method for minimizing the input 'minimizable' array. The document and index minimizers both rely on this function. * Returns a list of essential indexes in the original input iterable. * * @param controlErr The fuzzer error we originally observed and wish to preserve during the minimization. * @param cmdFn The test function that allows us to determine the effect of removing an item. */ minimize(controlErr, cmdFn) { for (let i = 0; i < this.length; i++) { // Try removing the i-th element. this.remove(i); const candidateError = cmdFn(); if (!candidateError || controlErr.comparable() !== candidateError.comparable()) { // Element i is essential if we either don't get an error, or we get a different error than we were expecting. this.replace(i); } } return this.essentialIndexes; } } class MinimizableDocs extends Minimizable { constructor(dbs, collectionNames, docs) { super(docs.length); this.dbs = dbs; this.collectionNames = collectionNames; this.docs = docs; } _remove(idx) { seedData.removeDocs(this.dbs, this.collectionNames, [this.docs[idx]]); } _replace(idx) { seedData.insertDocs(this.dbs, this.collectionNames, [this.docs[idx]]); } getMinimized(controlErr, cmdFn) { const minIdxs = this.minimize(controlErr, cmdFn); return minIdxs.map(i => this.docs[i]); } } function runDocumentMinimization(dbs, collectionNames, documents, controlErr, cmdFn) { return new MinimizableDocs(dbs, collectionNames, documents).getMinimized(controlErr, cmdFn); } function isAggregation(minimizedOutputs) { return 'aggregationList' in minimizedOutputs; } function formatMinimizedOutputs(minimizerOutputs) { // We need to use .tojson so that we can preserve BSON data types. // Using the shell's JSON.stringify will not preserve data types. if (isAggregation(minimizerOutputs)) { return { aggregationList: minimizerOutputs.aggregationList.map(aggregation => tojson(aggregation, '', true).slice(1, -1)), aggregationOptionsList: minimizerOutputs.aggregationOptionsList.map(aggregationOption => tojson(aggregationOption, '', true)), documents: minimizerOutputs.documents.map(document => tojson(document, '', true)), indexes: minimizerOutputs.indexes.map(index => tojson(index, '', true)), indexOptions: minimizerOutputs.indexOptions.map(indexOption => tojson(indexOption, '', true)), controlErr: minimizerOutputs.controlErr.replace(/(\n|\t)/gm, ''), stats: tojson(minimizerOutputs.stats, '', true), }; } else { return { queryList: minimizerOutputs.queryList.map(query => tojson(query, '', true)), documents: minimizerOutputs.documents.map(document => tojson(document, '', true)), indexes: minimizerOutputs.indexes.map(index => tojson(index, '', true)), indexOptions: minimizerOutputs.indexOptions.map(indexOption => tojson(indexOption, '', true)), shardKey: tojson(minimizerOutputs.shardKey, '', true), controlErr: minimizerOutputs.controlErr.replace(/(\n|\t)/gm, ''), stats: tojson(minimizerOutputs.stats, '', true), }; } } class IndexMinimizationError extends Error { constructor(msg) { super(msg); } } /** * Retrieves the output of $indexStats for every collection in every database as a matrix. * Note that for every db.coll, we have an array of IndexStats objects (one per index). */ function getIndexStats(dbs, colls) { const indexStats = []; for (const db of dbs) { indexStats.push([]); for (const coll of colls) { const ixStats = db[coll] .aggregate([ { $indexStats: {} }, // Ensure index stats are sorted in the same order. { $sort: { name: 1 } }, ]) .toArray(); indexStats[indexStats.length - 1].push(ixStats); } } return indexStats; } /** * Compare two outputs of $indexStats by number of index accesses. */ function diffIndexStats(newIndexStats, oldIndexStats) { const diff = []; const len = newIndexStats.length; // We should not have created any new indexes in the meantime. assert(newIndexStats.length === oldIndexStats.length); for (let i = 0; i < len; i++) { const newIx = newIndexStats[i]; const oldIx = oldIndexStats[i]; // Ensure we are comparing the same indexes. assert(newIx.name === oldIx.name, `Expected to be comparing with index ${oldIx.name}, but got index ${newIx.name}`); const newAccesses = newIx.accesses.ops - oldIx.accesses.ops; if (newAccesses === 0) { // This index was not used during the failing aggregation! continue; } // If we have any 'newAccesses', this index was used by the query. diff.push(newIx); } return diff; } /** * Converts from matrix of $indexStats per db.coll into the format consumed by the aggregation fuzzer, merging all the changed $indexStats entries together. */ function convertIxStatsToFuzzerFormat(ixStats) { const output = { indexes: [], options: [], }; // TODO TIG-4459: right now, we merge all used indexes across all colls/dbs, but we shouldn't. const seenIdxs = new Set(); for (let i = 0; i < ixStats.length; i++) { for (let j = 0; j < ixStats[i].length; j++) { for (let k = 0; k < ixStats[i][j].length; k++) { const ixStat = ixStats[i][j][k]; if (!seenIdxs.has(ixStat.name)) { output.indexes.push(ixStat.key); // Remove non-option fields in $indexStats spec. const fuzzerIdx = ixStat.spec; delete fuzzerIdx['v']; delete fuzzerIdx['name']; delete fuzzerIdx['key']; output.options.push(fuzzerIdx); seenIdxs.add(ixStat.name); } } } } return output; } function testHypothesis(controlErr, cmdFn) { const candidateError = cmdFn(); // Ensure the function fails again with the same error, otherwise throw. if (!candidateError) { throw new IndexMinimizationError('$indexStats minimization failed, aggregation did not fail on the second run.'); } else if (controlErr.comparable() !== candidateError.comparable()) { throw new IndexMinimizationError('$indexStats minimization failed, aggregation failed with a different error on the second run.'); } } /** * Attempts to minimize the list of 'actualIndexes' by comparing the output of $indexStats before and after a failing query is re-run. */ function minimizeIndexesViaIndexStats(dbs, colls, controlErr, cmdFn) { // Get the value of $indexStats at the end of the run. const oldIndexStats = getIndexStats(dbs, colls); // Run the query again to update $indexStats with a new value and ensure the function fails again with the same error. testHypothesis(controlErr, cmdFn); // Get the updated value of $indexStats. const newIndexStats = getIndexStats(dbs, colls); // Now compare the value of the two $indexStats calls to find out which indexes were used by this aggregation (if any). const usedIndexes = []; for (let dbIdx = 0; dbIdx < dbs.length; dbIdx++) { usedIndexes.push([]); for (let collIdx = 0; collIdx < colls.length; collIdx++) { const diff = diffIndexStats(newIndexStats[dbIdx][collIdx], oldIndexStats[dbIdx][collIdx]); usedIndexes[dbIdx].push(diff); } } return convertIxStatsToFuzzerFormat(usedIndexes); } class MinimizableIndexList extends Minimizable { constructor(dbs, collectionNames, actualIndexes) { super(actualIndexes.indexes.length); this.dbs = dbs; this.collectionNames = collectionNames; this.actualIndexes = actualIndexes; } _remove(idx) { // Make sure we don't try to drop the _id index! if (seedData.indexNameIndexKeyPattern(this.actualIndexes.indexes[idx]) !== '_id_1') { seedData.dropIndex(this.dbs, this.collectionNames, this.actualIndexes.indexes[idx], this.actualIndexes.options[idx]); } } _replace(idx) { // Make sure we don't try to create the _id index! if (seedData.indexNameIndexKeyPattern(this.actualIndexes.indexes[idx]) !== '_id_1') { seedData.createIndex({ dbs: this.dbs, collections: this.collectionNames, index: this.actualIndexes.indexes[idx], indexOption: this.actualIndexes.options[idx], }); } } getMinimized(controlErr, cmdFn) { const minIdxs = this.minimize(controlErr, cmdFn); return { indexes: minIdxs.map(i => this.actualIndexes.indexes[i]), options: minIdxs.map(i => this.actualIndexes.options[i]), }; } } /** * actualIndexes is a pair of two arrays - one with indexes, and one with corresponding options. * This function removes an index and its corresponding option from position 'i'. */ function removeIndexAndOption(actualIndexes, i) { // Below we perform a constant time removal of the index and its option from the // corresponding arrays. // First copy the last element of the array into the position of the index to be removed. // Then trim the array by popping the last element. // This works fine since these arrays are treated as sets, and index order doesn't matter. actualIndexes.indexes[i] = actualIndexes.indexes[actualIndexes.indexes.length - 1]; actualIndexes.indexes.pop(); actualIndexes.options[i] = actualIndexes.options[actualIndexes.indexes.length - 1]; actualIndexes.options.pop(); } function runIndexMinimizationViaIndexStats(dbs, collectionNames, actualIndexes, controlErr, cmdFn) { try { const outIndexes = minimizeIndexesViaIndexStats(dbs, collectionNames, controlErr, cmdFn); // Update actualIndexes, and try refining this further through regular index minimization, // in case we can still reduce the number of indexes. But first, we should drop all other indexes. // Don't bother if we haven't minimized at all. if (outIndexes.indexes.length < actualIndexes.indexes.length) { // Index creation is slow, but we only have up to 62 indexes, // so doing computation here to determine which indexes to drop is relatively cheaper // than dropping them all and recreating the rest. const outIndexesSet = new Set(outIndexes.indexes.map(kp => seedData.indexNameIndexKeyPattern(kp))); for (let i = 0; i < actualIndexes.indexes.length; i++) { const actualIdx = actualIndexes.indexes[i]; const actualIdxOpt = actualIndexes.options[i]; const idxName = seedData.indexNameIndexKeyPattern(actualIdx); // Make sure we don't try to drop the _id index! if (!outIndexesSet.has(idxName) && idxName !== '_id_1') { seedData.dropIndex(dbs, collectionNames, actualIdx, actualIdxOpt); removeIndexAndOption(actualIndexes, i); } } // Now, verify that we still get the error one last time. If we don't, we'll throw, // and the caller will default to regular index minimization. testHypothesis(controlErr, cmdFn); // We didn't throw- this means our hypothesis (that we can get away with fewer indexes) // was correct. return outIndexes; } } catch (err) { if (err instanceof IndexMinimizationError) { print('WARNING: ', err.message, ' Falling back to regular index minimization.'); } else { throw err; } } return actualIndexes; } /** * Entry point for index minimization. */ function runIndexMinimization(dbs, collectionNames, actualIndexes, controlErr, cmdFn, allowIndexMinimisationFallback = true) { // We can only use $indexStats when both control & experiment aggregations run to completion, // but return different results; otherwise, we can't rely on $indexStats being updated. if (controlErr.message.match(/Failed to find all results in .* for example/)) { actualIndexes = runIndexMinimizationViaIndexStats(dbs, collectionNames, actualIndexes, controlErr, cmdFn); } if (!allowIndexMinimisationFallback) { print('$indexStats minimization failed and traditional minimisation not permitted for testing mode; indexes not minimised.'); return actualIndexes; } // Otherwise, fallback to trying to remove indexes one at a time. return new MinimizableIndexList(dbs, collectionNames, actualIndexes).getMinimized(controlErr, cmdFn); } const shell$2 = globalThis; function writeOutput(minimizerOutputs) { const path = shell$2.pwd() + '/minimizer-outputs.json'; try { shell$2.removeFile(path); } catch (_) { // pass } shell$2.writeFile(path, JSON.stringify(formatMinimizedOutputs(minimizerOutputs))); } /** * Use the {libfuzz/minimizer} to find the minimal number of indexes and inserted * documents that could generate the same error and print them out. * @param dbs {shell.DB[]} Databases to run the command on. * @param controlErr {FuzzerError} The returned errors. * @param cmdFn {() => FuzzerError | null} A function that runs the command and could return the error. * @param actualIndexes {IndexList} The indexes created. */ function minimizeCommon(dbs, controlErr, cmdFn, actualIndexes, documentList, collectionNames, allowIndexMinimisationFallback = true) { const documents = runDocumentMinimization(dbs, collectionNames, documentList, controlErr, cmdFn); const minimizedIndexes = runIndexMinimization(dbs, collectionNames, actualIndexes, controlErr, cmdFn, allowIndexMinimisationFallback); return [ // Minimized results. { documents, indexes: minimizedIndexes.indexes, indexOptions: minimizedIndexes.options, controlErr: controlErr.toString(), }, // Minimizer statistics. { documents: { total: documentList.length, reduced: documents.length, factor: documents.length / documentList.length, }, indexes: { total: actualIndexes.indexes.length, reduced: minimizedIndexes.indexes.length, factor: minimizedIndexes.indexes.length / actualIndexes.indexes.length, }, }, ]; } /** * Minimizer entry point for the aggregation fuzzer. Wrapper for minimizeCommon(), with an extra parameter for the original aggregation list. */ function minimizeAggFuzzerOutputs(dbs, collectionNames, controlErr, cmdFn, actualIndexes, documentList, aggregationList, allowIndexMinimisationFallback) { // The pipeline and agg options are stored in the error, so just grab them. const badAggList = [controlErr.cmd]; const badAggListOptions = [controlErr.options]; const [common, stats] = minimizeCommon(dbs, controlErr, cmdFn, actualIndexes, documentList, collectionNames, allowIndexMinimisationFallback); const minimizerOutputs = { aggregationList: badAggList, aggregationOptionsList: badAggListOptions, stats: { aggs: { total: aggregationList.length, reduced: badAggList.length, factor: badAggList.length / aggregationList.length, }, ...stats, }, ...common, }; writeOutput(minimizerOutputs); } function generateLatestVersionInfo(branchName) { if (branchName === 'master') { return { isV81: true, major: 8, minor: 1 }; } const regex = /[0-9]/g; const version = branchName.match(regex); // Input needs to have 2 numbers, nothing else required. // For sanity, the format 'vX.Y' is recommended. if (!version || version.length < 2) { throw new Error('Branch version ' + branchName + " does not match the required format. Please enter a branch in the format 'v4.2' or enter 'master'."); } // !!!DEPRECATED!!! // 'isVdd' fields are deprecated! Use 'major' and 'minor' fields and versionCompare() function // instead. // Dynamically create keys of format 'isVdd'. const isKey = 'isV' + version[0] + version[1]; return { [isKey]: true, ['major']: +version[0], ['minor']: +version[1] }; } /** * Compares 'v1' to 'v2' and returns a number: * - less than zero if 'v1' is less than that version; * - greater than zero 'v1' is greater than that version; or * - zero if 'v1' is equal to that version. */ function versionCompare(v1, v2) { if (v1.major !== v2.major) return v1.major - v2.major; else return v1.minor - v2.minor; } const shell$3 = globalThis; /** * Function that populates 'generated' with the version of the server that it's running against. For * instance, if 'generated' will run against a 4.4 mongod, the 'latestVersion' property will be set * to the object {isV44: true, major: 4, minor: 4}. * * @param generated the generated fuzzer test to append a version to */ function populateVersion(generated) { shell$3.print('This fuzzer will override latestVersion'); var versionStr = shell$3.version(); shell$3.print('latestVersion ', versionStr); Object.assign(generated.latestVersion, generateLatestVersionInfo(versionStr)); shell$3.print('isLatest will throw errors'); Object.defineProperty(generated.latestVersion, 'isLatest', { get isLatest() { throw new Error( 'isLatest has been deprecated and removed. Use isVXY where X and Y are the major and minor version numbers respectively (ex: isV50). This property is dynamically created for all versions of MongoDB.' ); }, }); } const shell$4 = globalThis; /* Remove bannedKeys from options. */ const filterBannedOptions = (options, bannedKeys) => { const mutableOptions = Object.assign({}, options); bannedKeys.forEach(key => delete mutableOptions[key]); return mutableOptions; }; /* Takes a database and returns whether its version is less than 4.5 */ const supportsAPIParameters = (db) => { const supportsCompareBinVersions = typeof shell$4.MongoRunner.compareBinVersions === 'function'; if (supportsCompareBinVersions) { return shell$4.MongoRunner.compareBinVersions(db.version(), '4.5') >= 0; } return false; }; /* Keys that indicate apiParameters. */ const apiKeys = ['apiVersion', 'apiStrict', 'apiDeprecationErrors']; // Code referenced from NetworkX v2.3 // See notice in THIRD-PARTY-NOTICES.txt class SetMatchingImpl { constructor(matches, leftUnmatched, rightUnmatched) { this.matches = matches; this.leftUnmatched = leftUnmatched; this.rightUnmatched = rightUnmatched; } /** * Given an adjacency list of a bipartite graph, return a set with a maximum cardinality matching * using the Hopcroft-Karp algorithm and any nodes not used in the matching. * Parameters: An object, representative of adjacency lists of a bipartite graph. One adjacency list * is for the left set of the bipartite graph, and the second is for the right set. * Returns: An array of length 3. The first index of the array holds the matching (an object literal * with the keys representing left set and the values representing the right set). The second index * holds an array of free nodes remaining in the left set. The third index holds an array of free * nodes remaining in the right set. * For example: * GRAPH: * (LEFT) 0 1 2 3 * | / / / | * (RIGHT) 4 5 6 7 * LEFT ADJACENCY LIST: {0: [4], 1: [4], 2: [5] 3: [6, 7]} * RIGHT ADJACENCY LIST: {4: [0, 1], 5: [2], 6: [3], 7: [3]} * The resulting matching may look like: [{0: 4, 2: 5, 3: 6}, [1], [7]]. */ static match(leftAdjacencies, rightAdjacencies) { const leftResultSet = Object.keys(leftAdjacencies); const rightResultSet = Object.keys(rightAdjacencies); const leftMatches = {}; const rightMatches = {}; const distances = {}; const queue = []; if (leftResultSet.includes('undefined') || rightResultSet.includes('undefined')) { throw new Error('There is an index in the adjacency list named "undefined"'); } // Update the distances object to include the maximum possible distance from one node on the // left set to a free node in the left side. If a node's distance is 0, that node is a free // node. // Returns true if there are free nodes (and therefore paths to update) and false if otherwise. function breadthFirstSearch() { // Determine which nodes are free nodes. If free, push the node to the queue so we can // determine the maximum possible path later. leftResultSet.forEach(node => { if (!leftMatches[node]) { distances[node] = 0; queue.push(node); } else { distances[node] = Infinity; } }); // distances[maxPathLen] represents the maximum possible length for an augmenting path. // there are no free nodes to start the augmenting path, then the maximum possible length // will remain infinity. distances['maxPathLen'] = Infinity; // Determine the maximum possible distance from a node on the left set to a free node on // the left set. while (queue.length) { const node = queue.shift(); if (distances[node] < distances['maxPathLen']) { leftAdjacencies[node].forEach(edge => { const rightMatch = typeof rightMatches[edge] === 'undefined' ? 'maxPathLen' : rightMatches[edge]; if (distances[rightMatch] === Infinity) { distances[rightMatch] = distances[node] + 1; queue.push(rightMatch); } }); } } return distances['maxPathLen'] !== Infinity; } // Search for which edge to use for a particular node's matching. function depthFirstSearch(node) { if (typeof node === 'undefined' || node === 'maxPathLen') { return true; } const edges = leftAdjacencies[node]; for (let i = 0; i < edges.length; i++) { const rightMatch = typeof rightMatches[edges[i]] === 'undefined' ? 'maxPathLen' : rightMatches[edges[i]]; if (distances[rightMatch] === distances[node] + 1) { if (depthFirstSearch(rightMatch)) { rightMatches[edges[i]] = node; leftMatches[node] = edges[i]; return true; } } } distances[node] = Infinity; return false; } // The main loop. while (breadthFirstSearch()) { leftResultSet.forEach(node => { if (!leftMatches[node]) { depthFirstSearch(node); } }); } // Any nodes not used in the maximum cardinality matching set will listed after the set. const leftUnusedNodes = leftResultSet.filter(node => !leftMatches[node]); const rightUnusedNodes = rightResultSet.filter(node => !rightMatches[node]); return new SetMatchingImpl(leftMatches, leftUnusedNodes, rightUnusedNodes); } /** * Returns an empty SetMatching. */ static blankMatch() { return new SetMatchingImpl({}, [], []); } } const shell$5 = globalThis; /** * @param {number} lhs * @param {number} rhs * @returns {number} relative diffence between numbers */ function relativeDifference(lhs, rhs) { return (lhs - rhs) / Math.max(Math.abs(lhs), Math.abs(rhs)); } /** * @param {number} a * @param {number} b * @param {number} relativePrecision - consideres numbers equal if their relative difference is less then 1e-relativePrecision. * @returns {boolean} if numbers are approximately equal */ function numbersAlmostEqual(a, b, relativePrecision = 10) { if (a === b) { return true; } // Compare integers the same { const truncA = Math.trunc(a); const truncB = Math.trunc(b); if (truncA === a && truncB === b && truncA === truncB) { return true; } } const maxRelativeDifference = Math.pow(10, -relativePrecision); return Math.abs(relativeDifference(a, b)) < maxRelativeDifference; } /** * Given an object, returns whether it was constructed with a numeric object type. */ function hasNumericObjectType(obj) { const objTypes = ['NumberDecimal', 'NumberLong', 'NumberInt']; return obj && obj.constructor && objTypes.includes(obj.constructor.name); } /** * If obj is of numeric type, returns the result of comparing num and obj after converting num to * an object of obj's type; false otherwise. */ function compareNumbersOfDifferentTypes(num, obj) { if (hasNumericObjectType(obj)) { try { const numAsObject = new obj.constructor(num); return compareDocs(numAsObject, obj, new ComparisonState(false)).equal === 'equal'; } catch (e) { // If the attempted type conversion fails, log an error and return false. shell$5.printjsononeline('Could not perform numeric type conversion for ' + num + ' and ' + obj); } } return false; } /** * Returns true if 'a' is numeric type. */ function isNumericType(num) { return (num instanceof shell$5.NumberDecimal || num instanceof shell$5.NumberLong || num instanceof shell$5.NumberInt || typeof num === 'number'); } function knownDifference(a, b) { const PRECISION = 10; if (typeof a === 'number' && typeof b === 'number') { return numbersAlmostEqual(a, b, PRECISION); } else if (a instanceof shell$5.NumberDecimal && b instanceof shell$5.NumberDecimal) { return numberDecimalsEqualForFuzzer(a, b); } else if (isNumericType(a) && isNumericType(b)) { return numbersLookAlmostEqual(a, b, PRECISION); } if (hasNumericObjectType(a) && hasNumericObjectType(b)) { return numberDecimalsEqualForFuzzer(a, b); } return ((typeof a === 'number' && compareNumbersOfDifferentTypes(a, b)) || (typeof b === 'number' && compareNumbersOfDifferentTypes(b, a))); } function isNonNullObject(obj) { return typeof obj === 'object' && obj !== null; } function isPlainObject(doc) { return isNonNullObject(doc) && Object.getPrototypeOf(doc) === Object.prototype; } function isDocument(doc) { // JavaScript objects of class "BSON" are returned for BSON type "document" values from the // server's response. Native JavaScript Arrays are always returned for BSON type "array" values // from the server's response. return isPlainObject(doc) || Object.prototype.toString.call(doc) === '[object BSON]'; } function isArray(arr) { return isNonNullObject(arr) && Object.getPrototypeOf(arr) === Array.prototype; } const isNumberDecimalNaN = value => value instanceof shell$5.NumberDecimal && value.toJSON().$numberDecimal === 'NaN'; /* * The prefix path of a pair of documents/values being compared * For example for the follwoing result sets, * controlRes: [{a: [0, 1]}, {a, [2, 3]}, ... ] * | * * testRes: [{a: [0, 1]}, {a, [2, 3]}, ... ] * | * * at the point of comparison shown, the PrefixPathPair is {left:['0','a','1'], right:['0','a','1']} */ class PrefixPathPair { constructor(left = [], right = []) { this.left = left; this.right = right; shell$5.assert(this.left.length === this.right.length); } addPath(a, b) { shell$5.assert(this.left.length === this.right.length); const leftPaths = this.left.slice(); const rightPaths = this.right.slice(); leftPaths.push(a); rightPaths.push(b); return new PrefixPathPair(leftPaths, rightPaths); } } /* * Holds the comparison state at a particular point during comparison */ class ComparisonState { constructor(sorted, prefix = new PrefixPathPair(), resultCache = new Map()) { this.sortedComparison = sorted; this.prefix = prefix; this.resultCache = resultCache; } newState(a, b) { if (this.sortedComparison) { return this; } else { return new ComparisonState(this.sortedComparison, this.prefix.addPath(a, b), this.resultCache); } } } // Pre 8.0 we had a custom implementation of Map with a different API const isStandardMapType = Object.prototype.toString.call(new Map()) === '[object Map]'; // We don't always need to generate set matchings. This is a quick helper function to generate // a ResultComparison that says the two results are equal/non-equal with empty matchings. function emptyComparison(equalMatch) { return { equal: equalMatch, matchings: SetMatchingImpl.blankMatch() }; } /** * The numberDecimalsAlmostEqual() shell helper is only available in 6.0 and above. To silence most * fuzzer errors in versions below that, this function compares two numberic values and returns * true if their relative difference is less than 1e-digits. */ function numbersLookAlmostEqual(a, b, digits) { const getFloatByType = num => { if (num instanceof shell$5.NumberDecimal) { if (isNumberDecimalNaN(num)) { return NaN; } return Number.parseFloat(num.toJSON().$numberDecimal); } else if (num instanceof shell$5.NumberLong) { return Number.parseFloat(num.toJSON().$numberLong); } else if (num instanceof shell$5.NumberInt) { return Number.parseFloat(num.toJSON().$numberInt); } else if (typeof num === 'number') { return num; } else { throw new Error('num is not numeric.'); } }; const aFloat = getFloatByType(a); const bFloat = getFloatByType(b); if (Number.isNaN(aFloat) && Number.isNaN(bFloat)) { return true; } return numbersAlmostEqual(aFloat, bFloat, digits); } /** * When available, use numberDecimalsEqual() shell helper to compare 'NumberDecimal' types so that the comparsion works across notations. When not available, fall back to JavaScript-based solution. * @param a * @param b * @returns True if the numbers are almost equal. */ function numberDecimalsEqualForFuzzer(a, b) { // Type promotion to NumberDecimal rounds to 'DIGITS', so this number should never be more than 'DIGITS'. const DIGITS = 6; if (typeof shell$5.numberDecimalsAlmostEqual === 'function') { return shell$5.numberDecimalsAlmostEqual(a, b, DIGITS); } else if (typeof shell$5.numberDecimalsEqual === 'function') { return shell$5.numberDecimalsEqual(a, b) || numbersLookAlmostEqual(a, b, DIGITS); } else { return numbersLookAlmostEqual(a, b, DIGITS); } } function compareDocsInternal(a, b, state) { try { if (shell$5.bsonBinaryEqual(a, b)) { return emptyComparison('equal'); } } catch (e) { // ignore if size is too large shell$5.print(`bsonBinaryEqual threw exception: ${e}\nFalling back to compare subdocuments.`); } // Regarding the NaN comparisons: We intentionally don't consider NaN and shell.NumberDecimal('NaN') // to be equal because preserving type fidelity is one of properties we're interested in testing // about MongoDB. if (typeof a === 'number' && typeof b === 'number' && Number.isNaN(a) && Number.isNaN(b)) { return emptyComparison('equal'); } if (a instanceof shell$5.NumberDecimal && b instanceof shell$5.NumberDecimal && numberDecimalsEqualForFuzzer(a, b)) { return emptyComparison('equal'); } if (isNumberDecimalNaN(a) && isNumberDecimalNaN(b)) { return emptyComparison('equal'); } // The first call to this function will always pass in 2 arrays representing the 2 result sets // returned by mongo. We will only print out the unmatched top-level documents, so this is the // only case where we need to return non-empty SetMatchings. if (isArray(a) && isArray(b)) { const compareArrays = state.sortedComparison ? compareSortedArrays : compareOptimisticallySortedArrays; return compareArrays(a, b, state); } // If value is a document, check if all sorted children are equal. if (isDocument(a) && isDocument(b)) { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) { return emptyComparison('not-equal'); } for (const key of aKeys) { if (compareDocs(a[key], b[key], state.newState(key, key)).equal === 'not-equal') { // We could return the result of compareDocs() here, but we explicitly don't // to make it more clear that we will never look at the setMatchings. return emptyComparison('not-equal'); } } return emptyComparison('equal'); } // The documents are of different types. if (knownDifference(a, b)) { return emptyComparison('equal'); } return emptyComparison('not-equal'); } function compareDocs(a, b, state) { if (state.sortedComparison) { // We don't use the cache since we don't require it for sorted comaprisons and also since we // won't be able to reuse it for unsorted case. return compareDocsInternal(a, b, state); } const key = JSON.stringify(state.prefix); const resultCache = state.resultCache; if (isStandardMapType ? resultCache.has(key) : resultCache.get(key) !== null) { return resultCache.get(key); } const result = compareDocsInternal(a, b, state); if (isStandardMapType) { resultCache.set(key, result); } else { resultCache.put(key, result); } return result; } /** * Fast comparison of arrays. Does not attempt to check unsorted arrays. */ function compareSortedArrays(a, b, state) { // If one result set has more documents no further analysis needs to be done. if (a.length !== b.length) { return emptyComparison('not-equal'); } for (let i = 0; i < a.length; i++) { if (compareDocs(a[i], b[i], state.newState(i, i)).equal === 'not-equal') { return emptyComparison('not-equal'); } } return emptyComparison('equal'); } /** * This function serves both to determine whether two sets are equal, and to analyze * the arrays to determine which elements are different. Does an O(n) comparison for each element. */ function compareUnsortedArrays(a, b, state) { /** Returns an empty adjacency object, where the key is the index and the value is * an array of adjacencies. The offset indexes are necessary for the SetMatching algorithm. * * For example: * LEFT ADJACENCY LIST: {0: [], 1: [], 2: [] 3: []} * RIGHT ADJACENCY LIST: {4: [], 5: [], 6: [], 7: []} */ const initializeAdjacencies = (length, offset = 0) => { const adjList = {}; for (let i = 0; i < length; ++i) { adjList[i + offset] = []; } return adjList; }; const leftAdjacencies = initializeAdjacencies(a.length); const rightAdjacencies = initializeAdjacencies(b.length, a.length); const addAdjacency = (leftIndex, rightIdx) => { // Need to adjust rightIdx to the correct offset. const rightEntry = rightIdx + a.length; leftAdjacencies[leftIndex].push(`${rightEntry}`); rightAdjacencies[rightEntry].push(`${leftIndex}`); }; // First pass at making the adjacency lists. // Comparing every entry in 'a' to every entry in 'b' is quite slow, but // luckily we don't need to. If an entry in 'a' is perfectly identical to an entry in 'b', // we don't need to compare it to any other entries, as there won't be another more perfect // match. In this case, we consider a perfect match if the bson representation of two documents // is identical. // Keep track of perfect matches to not analyze them further. We only need 1 set for this // as the left-right matches are 1:1. const leftPerfectMatches = new Set(); // Keep track of the 'b' entries that don't have a perfect match. Once the entry has a perfect // match, we delete it to no longer compare it to entries in 'a'. const rightOmittingPerfectMatches = new Set(b.keys()); for (let leftIdx = 0; leftIdx < a.length; ++leftIdx) { const doc1 = a[leftIdx]; // Only compare against documents without a perfect match. for (const rightIdx of Array.from(rightOmittingPerfectMatches)) { const doc2 = b[rightIdx]; try { if (shell$5.bsonBinaryEqual(doc1, doc2)) { addAdjacency(leftIdx, rightIdx); leftPerfectMatches.add(leftIdx); rightOmittingPerfectMatches.delete(rightIdx); break; } } catch (_) { // Ignore if size is too large, 'compareDocs' in the second pass // will take care of the documents failed to compare using // 'shell.bsonBinaryEqual' including logging the error message. } } } // Second pass to finalize the adjacency lists. // Any entry in 'a' that doesn't have a perfect match needs to be compared to all other entries // in 'b', as we don't know whether a given match is the best match - this is what the // hopcroft-karp algorithm will figure out. for (let leftIdx = 0; leftIdx < a.length; ++leftIdx) { if (leftPerfectMatches.has(leftIdx)) { continue; } const doc1 = a[leftIdx]; for (const rightIdx of Array.from(rightOmittingPerfectMatches)) { const doc2 = b[rightIdx]; // if 'doc1' and 'doc2' contain arrays that are not equivalent, 'resultComparison' will // contain the SetMatching for those arrays. We will only return the SetMatching for // the top level results, so we discard the results for nested arrays. const resultComparison = compareDocs(doc1, doc2, state.newState(leftIdx, rightIdx)); if (resultComparison.equal === 'equal') { addAdjacency(leftIdx, rightIdx); } } } // Grab the result from the matching. const setMatchings = SetMatchingImpl.match(leftAdjacencies, rightAdjacencies); if (setMatchings.leftUnmatched.length || setMatchings.rightUnmatched.length) { return { equal: 'not-equal', matchings: setMatchings }; } else { return { equal: 'equal', matchings: setMatchings }; } } /** * This function first compares assuming array is sorted. If it fails, tries with the slower * `compareUnsortedArrays` function */ function compareOptimisticallySortedArrays(a, b, state) { let cmp = compareSortedArrays(a, b, state); if (cmp.equal === 'not-equal') { cmp = compareUnsortedArrays(a, b, state); } return cmp; } /* * Given an object as input, this function outputs an equivalent object where the fields within * documents appear in sorted order. This does not change the order of objects within an array. */ function sortDocumentFields(obj) { if (isArray(obj)) { for (let i = 0; i < obj.length; i++) { obj[i] = sortDocumentFields(obj[i]); } return obj; } else if (isDocument(obj)) { const ordered = Object.keys(obj) .sort() .reduce((sortedObj, key) => { sortedObj[key] = sortDocumentFields(obj[key]); return sortedObj; }, {}); return ordered; } return obj; } /** * Given an object as input, returns a deep copy of the object with null fields filtered out. */ function dropNullFields(obj) { if (isArray(obj)) { return obj.filter(val => val !== null).map(dropNullFields); } if (isDocument(obj)) { return Object.entries(obj) .filter(([, val]) => val !== null) .reduce((newObj, [key, val]) => ({ ...newObj, [key]: dropNullFields(val) }), {}); } shell$5.assert(obj !== null); return obj; } /** * @return number of documents that compare equal in both sets. If the two result sets are not * equal, either 0 is returned for blacklisted differences, or an error is thrown. */ function assertResultSetsEqual(res1, res2, desc1, desc2, assertResultDivergenceIsAcceptable = () => false, presortDocumentKeys = false, treatNullAsMissing = false) { // Result sets should always be arrays. shell$5.assert(isArray(res1) && isArray(res2)); if (presortDocumentKeys || treatNullAsMissing) { // If either is set to true, try the fast comparison assuming sorted first, // before modifying the documents const resultComparison = compareDocs(res1, res2, new ComparisonState(true)); if (resultComparison.equal === 'equal') { return res1.length; } } if (presortDocumentKeys) { res1 = sortDocumentFields(res1); res2 = sortDocumentFields(res2); } if (treatNullAsMissing) { res1 = dropNullFields(res1); res2 = dropNullFields(res2); } const resultComparison = compareDocs(res1, res2, new ComparisonState(false)); if (resultComparison.equal === 'equal') { return res1.length; } // Sets are unequal. Collect diagnostics, and throw if necessary. class ResultSetsUnequal extends Error { constructor(message = '', ...args) { super(); this.diagnostics = []; this.message = message; } addDiagnostics(diagnostics) { this.diagnostics.push(diagnostics); } printDiagnostics() { for (let i = 0; i < this.diagnostics.length; i++) { shell$5.print(this.diagnostics[i]); } } } let err; const setMatchings = resultComparison.matchings; if (setMatchings.leftUnmatched.length) { err = new ResultSetsUnequal(`Failed to find all results in ${desc1}, for example ` + shell$5.tojson(res1[setMatchings.leftUnmatched[0]])); err.addDiagnostics(`Unmatched docs in ${desc1}`); for (const nodeNum of setMatchings.leftUnmatched) { // tslint:disable-next-line: ban err.addDiagnostics(shell$5.tojson(res1[parseFloat(nodeNum)])); } } if (setMatchings.rightUnmatched.length) { if (!err) { // tslint:disable-next-line: ban const docPosition = parseFloat(setMatchings.rightUnmatched[0]) - res1.length; err = new ResultSetsUnequal(`Failed to find all results in ${desc2}, for example ` + shell$5.tojson(res2[docPosition])); } err.addDiagnostics(`Unmatched docs in ${desc2}`); for (const nodeNum of setMatchings.rightUnmatched) { // tslint:disable-next-line: ban const docPosition = parseFloat(nodeNum) - res1.length; err.addDiagnostics(shell$5.tojson(res2[docPosition])); } } if (assertResultDivergenceIsAcceptable()) { // don't throw, but still show diagnostics to inform devs err.printDiagnostics(); return 0; } throw err; } const shell$6 = globalThis; /** * Recursively checks if a javascript object contains a nested property key and returns the values. * NOTE: only recurses into other objects, array elements are ignored. * * This originates from mongo/jstests/libs/analyze_plan.js, and should * be kept consistent. */ function getNestedProperties(topLevel, key) { let accumulator = []; function traverse(object) { if (typeof object !== 'object') { return; } // eslint-disable-next-line guard-for-in for (const k in object) { if (k === key) { accumulator.push(object[k]); } traverse(object[k]); } } traverse(topLevel); return accumulator; } /** * Given an explain output, extract the query planner section. * * This originates from mongo/jstests/libs/analyze_plan.js, and should * be kept consistent. */ function getQueryPlanner(explain) { if ('queryPlanner' in explain) { const qp = explain.queryPlanner; // Sharded case. if ('winningPlan' in qp && 'shards' in qp.winningPlan) { return qp.winningPlan.shards[0]; } return qp; } // Can't use optional chaining here as it is not supported // for older mongo versions. const stages = explain.stages; shell$6.assert(stages, {msg: 'Explain missing stages', explain: explain}); const stage = stages[0]; shell$6.assert(stage, {msg: 'Explain missing stage', explain: explain}); const cursor = stage.$cursor; shell$6.assert(cursor, {msg: 'Explain missing cursor', explain: explain}); return cursor.queryPlanner; } /** * Extracts and returns an array of explain outputs for every shard in a sharded cluster; returns * the original explain output in case of a single replica set. * * This originates from mongo/jstests/libs/analyze_plan.js, and should * be kept consistent. */ function getAllNodeExplains(explain) { let shardsExplain = []; // If 'splitPipeline' is defined, there could be explains for each shard in the 'mergerPart' of // the 'splitPipeline', e.g. $unionWith. if (explain.splitPipeline) { const splitPipelineShards = getNestedProperties(explain.splitPipeline, 'shards'); shardsExplain.push(...splitPipelineShards.flatMap(Object.values)); } if (explain.shards) { shardsExplain.push(...Object.values(explain.shards)); } // NOTE: When shards explain is present in the 'queryPlanner.winningPlan' the shard explains are // placed in the array and therefore there is no need to call Object.values() on each element. const shards = (function() { if ( explain.hasOwnProperty('queryPlanner') && explain.queryPlanner.hasOwnProperty('winningPlan') ) { return explain.queryPlanner.winningPlan.shards; } return null; })(); if (shards) { shell$6.assert(Array.isArray(shards), shards); shardsExplain.push(...shards); } if (shardsExplain.length > 0) { return shardsExplain; } return [explain]; } /** * Returns a sub-element of the 'queryPlanner' explain output which represents a winning plan. * For sharded collections, this may return the top-level "winningPlan" which contains the shards. * To ensure getting the winning plan for a specific shard, provide as input the specific explain * for that shard i.e, queryPlanner.winningPlan.shards[shardNames[0]]. * * This originates from mongo/jstests/libs/analyze_plan.js, and should * be kept consistent. */ function getWinningPlan(queryPlanner) { // The 'queryPlan' format is used when the SBE engine is turned on. If this field is present, // it will hold a serialized winning plan, otherwise it will be stored in the 'winningPlan' // field itself. return queryPlanner.winningPlan.hasOwnProperty('queryPlan') ? queryPlanner.winningPlan.queryPlan : queryPlanner.winningPlan; } /** * Given the root stage of explain's JSON representation of a query plan ('root'), returns its * immediate children. * * Adapted from 'getPlanStages' in mongo/jstests/libs/analyze_plan.js. */ function* iterChildren(root) { if ('inputStage' in root) { yield root.inputStage; } if ('inputStages' in root) { yield* root.inputStages; } if ('queryPlanner' in root) { yield getWinningPlan(root.queryPlanner); } if ('thenStage' in root) { yield root.thenStage; } if ('elseStage' in root) { yield root.elseStage; } if ('outerStage' in root) { yield root.outerStage; } if ('innerStage' in root) { yield root.innerStage; } if ('queryPlan' in root) { yield root.queryPlan; } if ('child' in root) { yield root.child; } if ('leftChild' in root) { yield root.leftChild; } if ('rightChild' in root) { yield root.rightChild; } if ('shards' in root) { if (Array.isArray(root.shards)) { for (const shard of root.shards) { yield shard.hasOwnProperty('winningPlan') ? getWinningPlan(shard) : shard.executionStages; } } else { const shards = Object.keys(root.shards); for (const shard of shards) { yield root.shards[shard]; } } } } /** * Returns indexes used to cover the covered part of the given query plan. That is, returns * index names from all index scans that are not children (immediate or not) of a FETCH. */ function* iterIndexesForCoveredQuery(stage) { if (stage.stage === 'FETCH') { return; } // Since we're traversing top-down, we know that there is no FETCH on top // of this node. const scanStages = ['IXSCAN', 'DISTINCT_SCAN']; if (scanStages.some(x => x === stage.stage)) { yield stage.indexName; } for (const childStage of iterChildren(stage)) { yield* iterIndexesForCoveredQuery(childStage); } } /** * Given an explain output, returns indexes used for the covered part of the query. */ function getIndexesForCoveredQuery(topLevelExplain) { const result = []; for (const explain of getAllNodeExplains(topLevelExplain)) { const queryPlanner = 'winningPlan' in explain ? explain : getQueryPlanner(explain); const winningPlan = getWinningPlan(queryPlanner); result.push(...iterIndexesForCoveredQuery(winningPlan)); } return result; } const shell$7 = globalThis; /* * Check if the error is caused by invalid JavaScript and not by the MongoDB server. */ function isProgrammerError(err) { return (err instanceof RangeError || err instanceof ReferenceError || err instanceof SyntaxError || err instanceof TypeError); } function runAndCatchErrors(fn) { try { return [fn(), null]; } catch (err) { if (isProgrammerError(err)) { throw err; } else { return [null, err]; } } } /** * Retries the given command without indexes if the divergence between the results was caused by V2 * indexes not distinguishing between null and missing. */ function tryRetryCoveredQueryWithIndexesHidden(command, res1, res2, desc1, desc2, { getColl, runCommand, getExplainOutputAsObject }, assertResultDivergenceIsAcceptable, presortDocumentKeys = false) { const hiddenIndexes = []; let coveredCmdIdx; const results = [res1, res2]; const compareResults = ({ ignoreNulls = false }) => { const [numOfEqualDocs] = runAndCatchErrors(() => assertResultSetsEqual(results[0], results[1], desc1, desc2, assertResultDivergenceIsAcceptable, presortDocumentKeys, ignoreNulls)); return numOfEqualDocs; }; try { const kMaxNumberOfRetries = 2; for (let retryNum = 1; retryNum <= kMaxNumberOfRetries; retryNum++) { const indexesUsedToCoverQuery = [ getIndexesForCoveredQuery(getExplainOutputAsObject(0)), getIndexesForCoveredQuery(getExplainOutputAsObject(1)), ]; // Exit early if neither query was covered, or if both were covered. const [query1Covered, query2Covered] = indexesUsedToCoverQuery.map(x => x.length > 0); if (query1Covered === query2Covered) { break; } // We only want to retry the query that used the covered plan. The non-covered query is // assumed to be the correct one. if (coveredCmdIdx === undefined) { coveredCmdIdx = query1Covered ? 0 : 1; } // Only retry if the only difference between the result sets is null vs missing. if (compareResults({ ignoreNulls: true }) === null) { return { isCovered: true }; } const coll = getColl(coveredCmdIdx); for (const index of indexesUsedToCoverQuery[coveredCmdIdx]) { coll.hideIndex(index); hiddenIndexes.push(index); } shell$7.print('Diff for the command might be caused by v2 indexes not distinguishing between null and missing:', JSON.stringify(shell$7.tojson(command))); shell$7.print(`Retrying the command without the following indexes (attempt #${retryNum}):`, shell$7.tojson(hiddenIndexes)); // It's possible that the query fails because some intermediate stage // used a covered plan which returned missing instead of null for some field. // We don't want to retry on any errors since they might also be caused by some other bug. const [newResult] = runAndCatchErrors(() => runCommand(coveredCmdIdx)); if (newResult === null) { return { isCovered: true }; } results[coveredCmdIdx] = newResult; const numOfEqualDocs = compareResults({ ignoreNulls: false }); if (numOfEqualDocs !== null) { return { numOfEqualDocs, isCovered: true }; } } } finally { if (coveredCmdIdx !== undefined && hiddenIndexes.length) { const coll = getColl(coveredCmdIdx); for (const index of hiddenIndexes) { coll.unhideIndex(index); } } } return { isCovered: coveredCmdIdx !== undefined }; } function validateQueryResults({ command, err1, err2, res1, res2, desc1, desc2, assertErrorIsAcceptable, assertResultDivergenceIsAcceptable, getExplainOutputAsString, presortDocumentKeys = false, retryCoveredQueryFns = null, }) { var _a, _b; for (const { i, error, result } of [ { i: 1, error: err1, result: res1 }, { i: 2, error: err2, result: res2 }, ]) { if (error && result) { throw new Error('A command cannot have both errored and returned results;' + ` one of err${i}, and res${i} must be null`); } if (!error && !result) { throw new Error('A command must have errored or returned results;' + ` one of err${i}, and res${i} must not be null`); } } if (err1 && err2) { // Queries may not always error with the same error message due to // differences in the order in which expressions are evaluated. We treat all // cases where both commands fail with an error as equivalent. return 0; } // If only one command errors, check special cases depending on which fuzzer // is currently running using assertErrorIsAcceptable. try { if (err1 || err2) { assertErrorIsAcceptable(); return 0; } return assertResultSetsEqual(res1, res2, desc1, desc2, assertResultDivergenceIsAcceptable, presortDocumentKeys); } catch (e) { const [retryResult] = runAndCatchErrors(() => { if (retryCoveredQueryFns) { return tryRetryCoveredQueryWithIndexesHidden(command, res1, res2, desc1, desc2, retryCoveredQueryFns, assertResultDivergenceIsAcceptable, presortDocumentKeys); } return null; }); if (((_a = retryResult) === null || _a === void 0 ? void 0 : _a.numOfEqualDocs) !== undefined) { return retryResult.numOfEqualDocs; } const coveredQueryErrorHint = ((_b = retryResult) === null || _b === void 0 ? void 0 : _b.isCovered) ? ' (a covered plan was used)' : ''; // Print the most useful info on the first few lines. shell$7.print(`Unexpected failure for command, one line printout${coveredQueryErrorHint}: ${shell$7.tojsononeline(command)}`); shell$7.print(`Unexpected failure for command${coveredQueryErrorHint}: ${shell$7.tojson(command)}`); if (e.diagnostics) { e.printDiagnostics(); } if (getExplainOutputAsString) { shell$7.print(getExplainOutputAsString(0)); shell$7.print(getExplainOutputAsString(1)); } throw e; } } /** * Checks if the given 'entity' contains an element or has a key named 'operator'. If such a match * is found and 'subOperator' is specified, check that the matched item contains 'subOperator' as * well. Returns the element under 'operator', or 'subOperator' if it is specified. * * @param entity an aggregation pipeline or filter object to perform the check against. * @param operator a stage or expression to look for in 'entity' using dollar-prefix notation, * e.g., '$multiply'. * @param subOperator a stage or expression to look for within the object/array matching 'operator'. * @return the element in 'entity' under 'operator', if 'entity' contains a stage or an expression * with the specified name, else undefined. */ function getOperator(entity, operator, subOperator = undefined) { if (operator === undefined) { return entity; } if (Array.isArray(entity)) { for (const elem of entity) { const res = getOperator(elem, operator, subOperator); if (res !== undefined) { return res; } } } else if (typeof entity === 'object' && entity !== null) { for (const [key, value] of Object.entries(entity)) { const res = key === operator ? getOperator(value, subOperator) : getOperator(value, operator, subOperator); if (res !== undefined) { return res; } } } return undefined; } /** * Checks if the given 'entity' contains an element or has a key named 'operator'. * * If such a match is found and 'subOperator' is specified, check that the matched item contains * 'subOperator' as well. * * @param entity an aggregation pipeline or filter object to perform the check against. * @param operator a stage or expression to look for in 'entity' using dollar-prefix notation, * e.g., '$multiply'. * @param subOperator a stage or expression to look for within the object/array matching 'operator'. * @return boolean true, if 'entity' contains a stage or an expression with the specified name */ function containsOperator(entity, operator, subOperator = undefined) { return getOperator(entity, operator, subOperator) !== undefined; } /** * Returns the index of first occurrence of the stage in the pipeline or -1 if not found. * * @param pipeline an aggregation pipeline to perform the check against. * @param stage the name of a stage to search for in 'pipeline' including its '$' prefix (e.g., '$match'). * @param startIndex an index to start searching from (i.e., the search will not find a stage with index < 'i') * @return integer equal to the index of first occurence of the stage in the pipeline, -1 otherwise. */ function findIndexInPipeline(pipeline, stage, startIndex = 0) { if (Array.isArray(pipeline) && pipeline !== null && stage !== undefined) { for (let i = startIndex; i < pipeline.length; ++i) { const elem = pipeline[i]; if (Object.keys(elem)[0] === stage) { return i; } } } return -1; } /** * Checks if the given 'pipeline' contains the series of stages specified by the arguments. * * @param pipeline an aggregation pipeline to perform the check against. * @param start the name of a stage the series should start with, including its '$' prefix (e.g., '$match'). * @param end the name of a stage the series should end, including its '$' prefix (e.g., '$match'). * @param middle an array of stages permitted to be between the 'start' and 'end' stages. * @return boolean true, if the 'pipeline' contains a matching series of stages. */ function pipelineContainsOperatorSeries(pipeline, start, end, middle) { if (start === undefined || middle === undefined || end === undefined || pipeline === null) { return true; } if (Array.isArray(pipeline)) { let searchIndex = 0; while (true) { searchIndex = findIndexInPipeline(pipeline, start, searchIndex); if (searchIndex === -1) { break; } while (++searchIndex < pipeline.length) { const stageName = Object.keys(pipeline[searchIndex])[0]; if (stageName === end) { return true; } else if (middle.indexOf(stageName) === -1) { break; } } } } return false; } /** * Returns true if the given explain output signals that the query was executed with SBE, false * otherwise. */ function isExplainOutputFromSbe(explain) { return explain && explain.includes('slotBasedPlan'); } /** * Returns true if the two explain outputs were successful and came from different engines OR if * one explain produced an error and the other did not. In the latter case, we cannot know whether * the explain() which produced an error was using SBE or the classic engine so we relax our checks * for this case. */ function areExplainOutputsFromDifferentEnginesOrError(explain1, explain2) { return (isExplainOutputFromSbe(explain1) !== isExplainOutputFromSbe(explain2) || explain1.includes('errmsg') !== explain2.includes('errmsg')); } /** * Some expressions have known behavioral differences between the SBE and classic execution engines, * or can result in unstable result sets across multiple executions in SBE (e.g. due to use of * unordered data structures in SBE to represent arrays). Although we've decided these behavioral * differences are acceptable from a correctness standpoint, they can lead to spurious failures in * the agg and query fuzzers. This function is similar to 'checkErrorDueToKnownSbeDifferences()' * above, but applies to expressions which can result in two different result sets (as opposed to * cases where one side produces a result set and the other an error). * * Takes as input the aggregate or find command generated by the fuzzer ('cmd'), as well as the * explain output from both the control and experimental sides. * * Returns true if the results mismatch can be ignored and false otherwise. */ function checkResultsMismatchDueToKnownSbeDifferences(cmd, explain1, explain2) { // Ignore queries with these expressions only when comparing SBE against classic. const ignoreIfSbeVsClassicExpressions = [ // TODO SERVER-86419: SBE and classic behave differently for $bits* match expressions. // Until that's fixed, we relax testing for pipelines with these expressions. '$bitsAnyClear', '$bitsAnySet', '$bitsAllSet', '$bitsAllClear', ]; // Ignore queries with these expressions if at least one side of the comparison used SBE. const ignoreIfAnySbeExpressions = [ // The implementation of the set operators in SBE doesn't guarantee the order of the // resulting array, whereas the classic engine will always return a sorted array. This can // cause the results returned by the two engines to differ (beyond just array ordering) in // combination with other aggregation operators. See BF-26141 for an example. '$setDifference', '$setIntersection', '$setUnion', ]; if ((explain1 && explain2 && // We require that only one plan use SBE to determine whether we should ignore a // difference based on the SbeVsClassicExpressions. This is because it's possible for one // implementation to use SBE only for the data access (e.g. scan or ixscan/fetch) and // another plan to use SBE for the entire pipeline. isExplainOutputFromSbe(explain1)) || isExplainOutputFromSbe(explain2)) { for (const expression of ignoreIfSbeVsClassicExpressions) { if (containsOperator(cmd, expression)) { shell$7.print(`Ignoring mismatch in classic vs. SBE results due to known SBE difference with expression ${expression}`); return true; } } } if (isExplainOutputFromSbe(explain1) || isExplainOutputFromSbe(explain2)) { for (const expression of ignoreIfAnySbeExpressions) { if (containsOperator(cmd, expression)) { shell$7.print(`Ignoring mismatch in results due to known SBE difference with expression ${expression}`); return true; } } } return false; } /** * Wrapper class for exceptions generated by fuzzers. */ class FuzzerError extends Error { constructor(msg, cmd, machineErr, stack, options) { super(msg); this.cmd = cmd; this.options = options; this.machineErr = machineErr || msg; if (stack) { // A FuzzerError can mask the original exception, so we override the stack here. this.stack = stack; } } comparable() { return this.machineErr; } static exceptionWrapper(err, makeExFn) { const match = err.message.match(/Failed to find all results in .* for example/); let machineComparableErr; if (match) { machineComparableErr = match[0]; } return makeExFn(machineComparableErr); } } /** * Exception wrapper class for errors generated while comparing aggregation outputs. */ class AggError extends FuzzerError { constructor(msg, cmd, options, machineErr, stack) { super(msg, cmd, machineErr, stack, options); } static makeIfRequired(err, aggPipeline, aggOptions) { if (!(err instanceof AggError)) { return FuzzerError.exceptionWrapper(err, machineComparableErr => new AggError(err.message, aggPipeline, aggOptions, machineComparableErr, err.stack)); } return err; } } const shell$8 = globalThis; /** * Statistics for a single pipeline stage */ class AggStat { constructor(name) { this.name = name; this.validCount = 0; this.invalidCount = 0; this.lengthCount = {}; } addToLengthCount(length) { if (!(length in this.lengthCount)) { this.lengthCount[length] = 0; } this.lengthCount[length]++; } toString() { return [ '', `valid: ${this.validCount}`, `invalid: ${this.invalidCount}`, `doc count: ${shell$8.tojsononeline(this.lengthCount)}`, `name: "${this.name}"`, ].join('\t'); } } class AggFuzzerStats { constructor() { this.pipelineMap = {}; } recordStat(name, length, err1, err2) { // add to map if needed if (!(name in this.pipelineMap)) { this.pipelineMap[name] = new AggStat(name); } const stat = this.pipelineMap[name]; if (err1 || err2) { stat.invalidCount++; } else { stat.validCount++; stat.addToLengthCount(length); } } addDataPoint(pipeline, length, err1, err2) { throw new Error('addDataPoint() must be implemented by a subclass'); } generateReport() { const buffer = []; for (const key of Object.keys(this.pipelineMap)) { buffer.push(this.pipelineMap[key].toString()); } return buffer.join('\n'); } } /** * Map of statistics of all top-level expressions. */ class ExpressionFuzzerStats extends AggFuzzerStats { addDataPoint(pipeline, length, err1, err2) { const projectStage = pipeline[0]; const topLevelExpr = Object.keys(projectStage.$project.a)[0]; this.recordStat(topLevelExpr, length, err1, err2); } } /** * Map of statistics of all aggregation commands. */ class PipelineFuzzerStats extends AggFuzzerStats { addDataPoint(pipeline, length, err1, err2) { for (const stage of pipeline) { const stageName = Object.keys(stage)[0]; this.recordStat(stageName, length, err1, err2); } return; } } function getStatsForFuzzer(fuzzer) { if (fuzzer === 'agg_fuzzer' || fuzzer === 'change_stream_fuzzer' || fuzzer === 'change_stream_serverless_fuzzer' || fuzzer === 'change_stream_serverless_no_optimization_fuzzer') { return new PipelineFuzzerStats(); } else if (fuzzer === 'agg_expr_fuzzer') { return new ExpressionFuzzerStats(); } shell$8.print('WARNING: unknown fuzzer: ' + fuzzer + '. Falling back to generic stat recording.'); return new AggFuzzerStats(); } const shell$9 = globalThis; class AggCompareResults { constructor(aggGenerated) { this.aggGenerated = aggGenerated; this.stats = getStatsForFuzzer(aggGenerated.fuzzerName); } runExplainOnPipeline({ db, collName, aggregation, aggOptions, explainLevel }) { // If 'collName' is null, then the provided aggregation should be run directly on 'db', // so we use "aggregate: 1" to indicate the aggregation is collection agnostic. Otherwise, // we pass the given collection name. const aggCmd = Object.assign({ aggregate: collName ? collName : 1, pipeline: aggregation, cursor: {} }, aggOptions); return db.runCommand({ explain: aggCmd, verbosity: explainLevel }); } /** * This function is running $explain on aggregation mismatch when the * queries didn't error. * * @param db database instance * @param collName optional collection name * @param aggregation an aggregation pipeline which produced an error * @param aggOptions an aggregation options * @param explainLevel level of explain to use */ runExplainOnAggregationMismatch({ db, collName, aggregation, aggOptions, explainLevel = 'allPlansExecution', }) { const [expl, explErr] = runAndCatchErrors(() => this.runExplainOnPipeline({ db, collName, aggregation, aggOptions, explainLevel })); if (explErr) { return `\nAttempted to run the aggregate with explain(). There was an error running explain on version ${shell$9.tojson(db.version())}: [${shell$9.tojson(explErr)}]`; } return ('\nWARNING: explain plans may not be the same ones as the plan chosen in the real query!' + `\nExplain method for version ${db.description} aggregation:` + shell$9.tojson(expl)); } /** * This function will be called if either err1 or err2 is not null. */ ignoreAggFuzzerErrors() { // This function is called when a one side error occurs in the aggregation fuzzer. // It is supposed to throw an exception if this is an error that should not be // ignored. However, this will never be the case, because we always ignore one // side errors in the aggregation fuzzer. Therefore, this function does nothing. } /** * Loads the last entry from the 'system.profile' collection on the * given database instances filtering by the specified collection name * and the command type, which should be either aggregate or find. * * @param dbs database instances * @param collName collection name * @return an array with two json documents corresponding to each * profile entry */ loadSystemProfiles(dbs, collName) { const profile1 = dbs[0].system.profile .find({ ns: `${dbs[0].getName()}.${collName}`, $or: [{ 'command.aggregate': { $exists: true } }, { 'command.find': { $exists: true } }], }) .sort({ ts: -1 }) .limit(1) .toArray()[0]; const profile2 = dbs[1].system.profile .find({ ns: `${dbs[1].getName()}.${collName}`, $or: [{ 'command.aggregate': { $exists: true } }, { 'command.find': { $exists: true } }], }) .sort({ ts: -1 }) .limit(1) .toArray()[0]; return [profile1, profile2]; } /** * @param dbs database instances * @param collName collection name * @param pipeline an aggregation pipeline which produced a divergence * @param aggOptionsForEachDB An object containing any options passed to * the aggregation. For example, a collation. * @param getExplainOutput (optional) a function to get the explain * output as a string dump. * @return Boolean true means the differing result is expected and can * be safely ignored. Boolean false means we can't safely blacklist. The * caller is responsible for throwing an error if we can't blacklist * here. */ assertAggFuzzerResultDivergenceIsAcceptable(dbs, collName, pipeline, aggOptionsForEachDB, getExplainOutput) { const groupingStages = ['$count', '$sortByCount', '$group', '$bucket', '$bucketAuto']; if (!containsOperator(pipeline, '$changeStream')) { // Details on non-change stream aggregations are logged to the // systems.profile collection. This retrieves the profiles for the // current aggregation. const profiles = this.loadSystemProfiles(dbs, collName); const profile1 = profiles[0]; const profile2 = profiles[1]; // If we have an index on a field that not all documents have, a // 'null' value will be generated for documents that don't have // the field. If we then have a query that can be covered, the // 'null' value will be returned for those documents rather than // a missing document. TODO: remove once SERVER-23229 has been // fixed. const agg1Covered = profile1.docsExamined === 0 && profile1.nreturned > 0; const agg2Covered = profile2.docsExamined === 0 && profile2.nreturned > 0; // Only blacklist if one of the two aggregations was covered. If // both or neither were covered then it would not be an instance // of SERVER-23229 and should fail. if (agg1Covered !== agg2Covered) { return true; } } // Blacklist divergences caused by the newly added optimization // SERVER-33966 for subsequent $sort stages, including instances // with limit or match stages in between. if (pipelineContainsOperatorSeries(pipeline, '$sort', '$sort', [ '$match', '$limit', ])) { return true; } // Accept divergences between versions 4.2 and 4.4 in presence // of a $set stage and a grouping stage. The results may differ // as $set stage in 4.2 under certain conditions may add new // fields to the output document in a different order, so // grouping on such documents may differ between the two // versions. See SERVER-48403 for details. if (this.aggGenerated.diffTestingMode === 'version' && this.aggGenerated.latestVersion.isV44) { const setStages = ['$addFields', '$set']; for (const setStage of setStages) { for (const groupingStage of groupingStages) { if (containsOperator(pipeline, setStage) && containsOperator(pipeline, groupingStage)) { return true; } } } } // $setUnion is a commutative operator which allows upper grouping changes to change order // of operands of the operator. ExpressionSetUnion::evaluate uses ValueSet to build a set of // values. ValueSet uses semantics of Value type when all numeric types can be considered // equal as long as they have the same value. This means that NumberLong("0") is equal // Decimal("-0") since they are both numeric and have the same value. if (this.aggGenerated.diffTestingMode === 'optimization' || this.aggGenerated.diffTestingMode === 'timeseries' || this.aggGenerated.diffTestingMode === 'blockprocessing') { const setStages = ['$setUnion', '$setIntersect']; for (const groupingStage of groupingStages) { for (const setStage of setStages) { if (containsOperator(pipeline, groupingStage, setStage)) { return true; } } } } // In 4.2 a bug fix was introduced to correctly consider the // collation for plan cache keys and index filters. This was not // backported to 4.0 and may result in different behavior across // the two versions. Check if the failing command has a // non-simple collation and relax the fuzzer. if (this.aggGenerated.diffTestingMode === 'version' && this.aggGenerated.latestVersion.isV42 && Object.keys(aggOptionsForEachDB[0].collation).length > 0) { return true; } if (getExplainOutput && checkResultsMismatchDueToKnownSbeDifferences(pipeline, getExplainOutput(0), getExplainOutput(1))) { return true; } // Although there are some known differences between the classic engine and SBE with // regards to field ordering in $project, the fuzzer is relaxed with respect to field // ordering. But when there is a $project followed by some other operators (e.g., // $sortByCount, $sort or $group), the field order does matter because documents with the // same values that are in different order will be considered different. This leads to // failures where we get a result mismatch because the documents were grouped differently. // As there are very many such combination of operators that can produce a completely // different result-sets due to this difference, we ignore such differences for pipelines // that contain `$project`. if ((this.aggGenerated.diffTestingMode === 'version' || this.aggGenerated.diffTestingMode === 'optimization' || this.aggGenerated.diffTestingMode === 'columnstore') && containsOperator(pipeline, '$project') && areExplainOutputsFromDifferentEnginesOrError(getExplainOutput(0), getExplainOutput(1))) { return true; } // $project, $addFields, and $set may produce different field orders under SBE vs. // the classic engine, and grouping stages are sensitive to field order. // // Because of this, we need the fuzzer to ignore SBE vs. classic differences if we // have a grouping stage in the pipeline and we have $project, $addFields, or $set // in the pipeline. We also need to ignore differences if both explains show SBE, // since part of the pipeline could have been pushed down to classic. if (getExplainOutput) { const bothSbe = isExplainOutputFromSbe(getExplainOutput(0)) && isExplainOutputFromSbe(getExplainOutput(1)); const neitherHasError = !getExplainOutput(0).includes('errmsg') && !getExplainOutput(1).includes('errmsg'); if (areExplainOutputsFromDifferentEnginesOrError(getExplainOutput(0), getExplainOutput(1)) || (bothSbe && neitherHasError)) { const projectOrAddFieldsStages = ['$project', '$addFields', '$set']; let hasGroupingStage = false; for (const stage of groupingStages) { if (containsOperator(pipeline, stage)) { hasGroupingStage = true; break; } } let hasProjectOrAddFieldsStage = false; for (const stage of projectOrAddFieldsStages) { if (containsOperator(pipeline, stage)) { hasProjectOrAddFieldsStage = true; break; } } if (hasGroupingStage && hasProjectOrAddFieldsStage) { return true; } } } // Due to SERVER-60755, $graphLookup semantics around null/missing // changed sligthly which may cause a divergence in result sets starting // in v5.2. const isPostV52 = versionCompare(this.aggGenerated.latestVersion, { major: 5, minor: 3 }) >= 0; if (this.aggGenerated.diffTestingMode === 'version' && isPostV52 && containsOperator(pipeline, '$graphLookup')) { return true; } // Due to SERVER-63079, there is a difference in $setWindowFields // behavior between versions 5.1 and 5.2+. This change was backported // to 5.0, meaning the behavior on 5.0 and 5.2+ agrees but is different // than the behavior on 5.1. After $setWindowFields where the output is // an object that already exists in an input doc, on 5.0/5.2+ the input // doc will lose all of its pre-existing fields under the object, // whereas on 5.1 the existing fields are preserved along with whatever // $setWindowFields adds. This change is desirable, so we want the // fuzzer to ignore result divergences in this case. if (this.aggGenerated.diffTestingMode === 'version') { const setWindowFields = getOperator(pipeline, '$setWindowFields'); // Only ignore the divergence if exactly one of the dbs is on 5.1. const is51 = dbVersion => shell$9.MongoRunner.compareBinVersions(dbVersion, '5.1') === 0; if (setWindowFields && setWindowFields.output.hasOwnProperty('obj') && is51(dbs[1].version()) !== is51(dbs[0].version())) { return true; } } // If we encounter mismatched results before version 6.1 in optimization fuzzer, or across // pre and post version 6.1 in multiversion fuzzer, and the pipeline contains a $add // or $multiply expression, then it is most likely due to the optimization issue described // in SERVER-63099. As it is undesired to change pre-existing behavior, this fix won't // be backported. if (containsOperator(pipeline, '$add') || containsOperator(pipeline, '$multiply')) { const isPre61 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 6, minor: 1 }) < 0; const isOptimization = this.aggGenerated.diffTestingMode === 'optimization' || this.aggGenerated.diffTestingMode === 'blockprocessing' || this.aggGenerated.diffTestingMode === 'timeseries'; if ((isOptimization && isPre61(dbs[0].version()) && isPre61(dbs[1].version())) || isPre61(dbs[0].version()) !== isPre61(dbs[1].version())) { return true; } } const isOptimizablePattern = () => pipelineContainsOperatorSeries(pipeline, '$sort', '$group', [ '$match', '$project', ]) || pipelineContainsOperatorSeries(pipeline, '$sort', '$bucket', [ '$match', '$project', ]); // If we encounter mismatched results in 8.0 or later in optimization fuzzer, or across // 8.0 or later and non-8.0 or later in multiversion fuzzer, and the pipeline contains a // $stdDevPop or $stdDevSamp and a $first or $last accumulator, then it is most likely // due to the optimization issue described in SERVER-89479. if (isOptimizablePattern() && (containsOperator(pipeline, '$stdDevPop') || containsOperator(pipeline, '$stdDevSamp') || containsOperator(pipeline, '$sum') || containsOperator(pipeline, '$avg')) && (containsOperator(pipeline, '$first') || containsOperator(pipeline, '$last'))) { const isV80OrPostV80 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 8, minor: 0 }) >= 0; const isOptimization = this.aggGenerated.diffTestingMode === 'timeseries'; if ((isOptimization && isV80OrPostV80(dbs[0].version()) && isV80OrPostV80(dbs[1].version())) || isV80OrPostV80(dbs[0].version()) !== isV80OrPostV80(dbs[1].version())) { return true; } } // The `DoubleDoubleSummation` issues described in SERVER-65735 and SERVER-67282 were backported // as dependencies of SERVER-75287 to version 6.0. Therefore, the results of these operations across // version 6.0 will result in divergences. They won't be backported to any earlier versions, so we // should ignore the fuzzer errors. if (containsOperator(pipeline, '$add') || containsOperator(pipeline, '$subtract')) { const isPre60 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 6, minor: 0 }) < 0; const onlyOnePre60 = isPre60(dbs[0].version()) !== isPre60(dbs[1].version()); if (this.aggGenerated.diffTestingMode === 'version' && onlyOnePre60) { return true; } } // SERVER-68434 fixed an issue where the planner incorrectly uses a cached partial ixscan // which does not cover the predicate. This issue was fixed on v7.0+ and backported to v6.0, // but it was not backported to v6.3 due to EOL. It is most likely to manifest with $lookup // queries because the subpipelines are likely to create and activate plan cache entries. // // Only ignore the divergence if exactly one of the dbs is on 6.3. const is63 = dbVersion => shell$9.MongoRunner.compareBinVersions(dbVersion, '6.3') === 0; if (this.aggGenerated.diffTestingMode === 'version' && containsOperator(pipeline, '$lookup') && is63(dbs[1].version()) !== is63(dbs[0].version())) { return true; } // SERVER-57321 fixed $mod handling of NaN, Infinity, and large values and was backported to // v4.4 but not v4.2 or v4.0, which are both already EOL. Thus ignore discrepancies in // pipelines containing $mod if the experimental db is v4.4, as this runs against v4.2 as // the control db. // // TODO TIG-4486: Remove this case once v4.4 is EOL. if (this.aggGenerated.diffTestingMode === 'version' && this.aggGenerated.latestVersion.isV44 && containsOperator(pipeline, '$mod')) { return true; } // SERVER-82168 fixed a semantic issue with $rank/$denseRank operator in $setWindowFields. // The results may change to be consistent with the specified sort order. // // This change happens in 8.0, so any difference between 8.0 and previous versions are // considered acceptable. if (containsOperator(pipeline, '$rank') || containsOperator(pipeline, '$denseRank')) { const isPre80 = dbVersion => shell$9.MongoRunner.compareBinVersions(dbVersion, '8.0') < 0; const onlyOnePre80 = isPre80(dbs[0].version()) !== isPre80(dbs[1].version()); if (this.aggGenerated.diffTestingMode === 'version' && onlyOnePre80) { return true; } } // SERVER-85337 fixed a semantic issue with $first/$firstN/$last/$lastN accumulators in $bucketAuto // and SERVER-87459 fixed a semantic issue with $mergeObjects accumulator in $bucketAuto. // The results may change to be consistent with the specified sort order. // // This change happens in 8.0, so any difference between 8.0 and previous versions are // considered acceptable. const positionalAccumulators = ['$first', '$firstN', '$last', '$lastN', '$mergeObjects']; // SERVER-81571 changed sorter.cpp to use a normal sort instead of stable sort. As a result, // $bucketAuto might return different output if the same key spans across multiple buckets. // These changes are expected and can be safely ignored for order-insensitive accumulators. const orderInsensitiveAccumulators = [ '$max', '$maxN', '$min', '$minN', // The official docs for $push and $accumulator don't promise that they merge // documents in order. '$push', '$accumulator', // With the current implementation the output of $stdDevPop/Samp can be different // with different input order '$stdDevPop', '$stdDevSamp', ]; if (containsOperator(pipeline, '$bucketAuto')) { for (const accumulator of positionalAccumulators.concat(orderInsensitiveAccumulators)) { if (containsOperator(pipeline, accumulator)) { const isPre80 = dbVersion => shell$9.MongoRunner.compareBinVersions(dbVersion, '8.0') < 0; const onlyOnePre80 = isPre80(dbs[0].version()) !== isPre80(dbs[1].version()); if (this.aggGenerated.diffTestingMode === 'version' && onlyOnePre80) { return true; } } } } // SERVER-86419 updated the behavior of $bitsAllSet, $bitsAnySet, $bitsAllClear and // $bitsAnyClear to match documentation in the case of NumberDecimals. Specifically, // NumberDecimals that don't fit into a 64-bit integer and non-integral values are // no longer matched by default. // // This change was not backported to 7.2 and 4.4 so any version differences // in mixed 7.2/7.3 and 4.4/5.0 clusters is considered acceptable. // TODO: DEVPROD-7417 remove the suppression once 4.4 and 7.2 are EOL. if (this.aggGenerated.diffTestingMode === 'version' && (containsOperator(pipeline, '$bitsAllSet') || containsOperator(pipeline, '$bitsAnySet') || containsOperator(pipeline, '$bitsAllClear') || containsOperator(pipeline, '$bitsAnyClear'))) { // Only one is 4.4 or 7.2. const is44 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 4, minor: 4 }) === 0; const is72 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 7, minor: 2 }) === 0; if (is44(dbs[0].version()) !== is44(dbs[1].version()) || is72(dbs[0].version()) !== is72(dbs[1].version())) { return true; } } // SERVER-90958 fixes the behavior of $bucketAuto to group documents with identical bucket // keys into same buckets, which was not being done in some cases. The fix has been // backported till 7.0 so ignore differences in results in pre and post 7.0 versions if (this.aggGenerated.diffTestingMode === 'version' && containsOperator(pipeline, '$bucketAuto', 'granularity')) { const isPre70 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 7, minor: 0 }) < 0; const onlyOnePre70 = isPre70(dbs[0].version()) !== isPre70(dbs[1].version()); if (onlyOnePre70) { return true; } } // SERVER-81571 replaces stable sort with normal sort which can lead to different results // pre and post 8.0 due to different bucket composition in presence of collation. if (this.aggGenerated.diffTestingMode === 'version' && containsOperator(pipeline, '$bucketAuto') && aggOptionsForEachDB[0] && aggOptionsForEachDB[0].collation && Object.keys(aggOptionsForEachDB[0].collation).length > 0) { const isPre80 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 8, minor: 0 }) < 0; const onlyOnePre80 = isPre80(dbs[0].version()) !== isPre80(dbs[1].version()); if (onlyOnePre80) { return true; } } // SERVER-87589's rewrite for timeseries queries may cause different results when there's an // case-insensitive collation option. This feature has been introduced since 8.0. const isAdjacentSortGroupRewritable = () => (pipelineContainsOperatorSeries(pipeline, '$sort', '$group', []) || pipelineContainsOperatorSeries(pipeline, '$sort', '$bucket', [])) && (containsOperator(pipeline, '$first') || containsOperator(pipeline, '$last')); const hasCaseInsensitiveCollation = aggOption => aggOption && 'collation' in aggOption && 'strength' in aggOption.collation && aggOption.collation.strength <= 2; if (this.aggGenerated.diffTestingMode === 'timeseries' && isAdjacentSortGroupRewritable() && (hasCaseInsensitiveCollation(aggOptionsForEachDB[0]) || hasCaseInsensitiveCollation(aggOptionsForEachDB[1]))) { const isPre80 = dbVersion => versionCompare(generateLatestVersionInfo(dbVersion), { major: 8, minor: 0 }) < 0; if (!isPre80(dbs[0].version()) && !isPre80(dbs[1].version())) { return true; } } return false; } /** * Runs an aggregation pipeline. Unlike the shell helper method * aggregate(), this function includes API parameters with getMore. * * @param db a Database * @param collName collection name * @param pipeline an aggregation pipeline * @param aggOptions an aggregation options * @return Array of documents */ runOneAggregation(db, collName, pipeline, aggOptions) { const cmd = Object.assign({ aggregate: collName ? collName : 1, pipeline, cursor: {}, }, aggOptions); let reply = db.runCommand(cmd); shell$9.assert.commandWorked(reply); let result = reply.cursor.firstBatch; let currBatch = reply.cursor.firstBatch; while (reply.cursor.id.valueOf() !== 0 && currBatch.length !== 0) { const ns = reply.cursor.ns; const getMore = { getMore: reply.cursor.id, collection: ns.substr(ns.indexOf('.') + 1), }; for (const option of [ 'apiVersion', 'apiStrict', 'apiDeprecationErrors', 'batchSize', 'maxTimeMS', 'txnNumber', 'autocommit', ]) { if (aggOptions.hasOwnProperty(option)) { getMore[option] = aggOptions[option]; } } reply = db.runCommand(getMore); shell$9.assert.commandWorked(reply); result = result.concat(reply.cursor.nextBatch); currBatch = reply.cursor.nextBatch; } return result; } runAggregations(dbs, aggCollName, aggregations, aggOptions) { for (let i = 0; i < dbs.length; i++) { const db = dbs[i]; const version = db.version(); const port = db.getMongo().host.replace(/.*:/, ''); let description = `MongoDB ${version} on port ${port}`; // Attempt to provide additional description to each db. const indexMeaning = { version: ['patch version', '"old" version'], optimization: ['optimizations turned on', 'disabled optimizations'], timeseries: ['optimizations turned on', 'disabled optimizations'], blockprocessing: ['regular timeseries', 'blockprocessing turned on'], sharded: ['non-sharded deployment', 'sharded cluster'], wildcard: ['with wildcard index', 'without wildcard index'], columnstore: ['with columnstore index', 'without columnstore index'], ['bonsai_m2_vs_sbe_stagebuilders']: ['tryBonsai', 'trySbeEngine'], ['bonsai_m2_vs_classic']: ['tryBonsai', 'forceClassicEngine'], }; const diffTestingMode = this.aggGenerated.diffTestingMode; const meanings = indexMeaning[diffTestingMode]; shell$9.assert(meanings !== undefined, `Unexpected diff testing mode "${diffTestingMode}"`); const meaning = meanings[i]; if (meaning) { description += ` (${meaning})`; } db.description = description; } const runPipelineFn = (db, collName, pipeline, options) => this.runOneAggregation(db, collName, pipeline, options); return this.runPipelinesAndCompareResults(dbs, aggCollName, aggregations, aggOptions, runPipelineFn); } /** * @returns {MinimizerError[]} errors */ runPipelinesAndCompareResults(dbs, collName, aggregations, aggOptions, runPipelineFn) { /* We must check whether apiVersion parameters are supported. */ const dbsSupportAPIParameters = dbs.map(db => supportsAPIParameters(db)); let firstException = null; // This array tracks each pipeline and the time it took to compare results for that pipeline for debugging purposes. const comparisonTimeStats = []; let totalComparisonTime = 0; for (let i = 0; i < aggregations.length; i++) { const aggregation = aggregations[i]; // There are a few cases when we don't want to add maxTimeMS to the aggregations. // First, maxTimeMS has different semantics for change stream pipelines. Also, for // the Bonsai M2 fuzzers, the agg grammar is very limited. We expect that these // pipelines won't be long-running, so it should be safe to omit the maxTimeMS param // to side-step the issue where getMores are not permitted to have maxTimeMS set. // TODO TIG-4453: In the future, Bonsai should not be special-cased here. Remove this // special-casing for Bonsai once getMores run as expected. const omitMaxTimeMS = containsOperator(aggregation, '$changeStream') || this.aggGenerated.diffTestingMode === 'bonsai_m2_vs_classic' || this.aggGenerated.diffTestingMode === 'bonsai_m2_vs_sbe_stagebuilders'; const baseAggOption = omitMaxTimeMS ? aggOptions[i] : Object.assign({ maxTimeMS: 30 * 1000 }, aggOptions[i]); const aggOptionsForEachDB = dbsSupportAPIParameters.map(dbSupportsAPIParameters => dbSupportsAPIParameters && !containsOperator(aggregation, '$text') ? baseAggOption : filterBannedOptions(baseAggOption, apiKeys)); shell$9.print('*********************************'); shell$9.print('Running Aggregation: ' + i); if (this.aggGenerated.debug) { shell$9.print(shell$9.tojson({ aggregation, aggregationOptions: aggOptionsForEachDB, })); } shell$9.print('*********************************'); const runCommand = (cmdIdx) => runPipelineFn(dbs[cmdIdx], collName, aggregation, aggOptionsForEachDB[cmdIdx]); const [res1, err1] = runAndCatchErrors(() => runCommand(0)); const [res2, err2] = runAndCatchErrors(() => runCommand(1)); if (this.aggGenerated.debug) { if (res1) { shell$9.print('Aggregation results:\n Result count: ' + res1.length + '\n First several documents (up to 5): ' + shell$9.tojson(res1.slice(0, 5))); } } const errMsg1 = err1 ? err1.message : null; const errMsg2 = err2 ? err2.message : null; // The order of fields in the output documents from a timeseries collection is more // variable than it is for non-timeseries collections. We can improve comparison // speed by sorting the document keys beforehand. const presortDocumentKeys = this.aggGenerated.diffTestingMode === 'timeseries'; const getExplainOutputAsString = (cmdIdx) => this.runExplainOnAggregationMismatch({ db: dbs[cmdIdx], collName, aggregation, aggOptions: aggOptionsForEachDB[cmdIdx], }); const getExplainOutputAsObject = (cmdIdx) => this.runExplainOnPipeline({ db: dbs[cmdIdx], collName, aggregation, aggOptions: aggOptionsForEachDB[cmdIdx], explainLevel: 'queryPlanner', }); const getColl = (cmdIdx) => dbs[cmdIdx].getCollection(collName); let length; try { const startingResultCompare = Date.now(); length = validateQueryResults({ command: aggregation, err1, err2, res1, res2, desc1: dbs[0].description, desc2: dbs[1].description, assertErrorIsAcceptable: fn => this.ignoreAggFuzzerErrors(), assertResultDivergenceIsAcceptable: () => this.assertAggFuzzerResultDivergenceIsAcceptable(dbs, collName, aggregation, aggOptionsForEachDB, getExplainOutputAsString), getExplainOutputAsString, presortDocumentKeys, retryCoveredQueryFns: { getExplainOutputAsObject, getColl, runCommand }, }); const finishedResultCompare = Date.now(); // Only print and keep track of the time taken to compare results if there wasn't an early return. if (length !== 0) { const resultComparisonTime = finishedResultCompare - startingResultCompare; shell$9.print(`Comparing results took ${resultComparisonTime} ms.`); comparisonTimeStats.push({ pipelineId: i, resultCompTime: resultComparisonTime }); totalComparisonTime += resultComparisonTime; } } catch (err) { firstException = AggError.makeIfRequired(err, aggregation, aggOptions[i]); break; } this.stats.addDataPoint(aggregation, length, errMsg1, errMsg2); } // Query Shape Stats are enabled only for the optimization fuzzer. We invoke $queryStats // here to ensure all Query Stats Store entries have query shapes that are re-parseable // at $queryStats read time. We only do so in version 6.0+ since that is the oldest version // that query stats is supported in. if (this.aggGenerated.diffTestingMode === 'optimization' && versionCompare(this.aggGenerated.latestVersion, { major: 6, minor: 0 }) >= 0) { dbs.forEach(db => { try { const numQueryStatsEntries = db .getSiblingDB('admin') .aggregate([ { $queryStats: { transformIdentifiers: { algorithm: 'hmac-sha-256', hmacKey: new shell$9.BinData(8, 'MjM0NTY3ODkxMDExMTIxMzE0MTUxNjE3MTgxOTIwMjE='), }, }, }, ]) .itcount(); shell$9.print(`Successfully retrieved ${numQueryStatsEntries} query stats entries from ${db.description}.`); } catch (err) { if (err.code === 224) { shell$9.print(`${db.description} returned a 'QueryFeatureNotAllowed' error from $queryStats. Run with the feature flag enabled to collect query stats entries.`); } else { throw err; } } }); } // Print statistics about the time taken to compare results. shell$9.print(`Total time spent to compare results: ${totalComparisonTime} ms`); const sortedTimesDesc = comparisonTimeStats.sort((obj1, obj2) => obj2.resultCompTime - obj1.resultCompTime); if (sortedTimesDesc.length >= 5) { shell$9.print('The pipeline IDs for the five slowest result comparisons:'); for (let i = 0; i < 5; i++) { shell$9.print(`\tResult comparison for pipeline with ID ${sortedTimesDesc[i].pipelineId} took ${sortedTimesDesc[i].resultCompTime} ms.`); } } return firstException; } /** * Runs the change stream pipelines with the specified options on the databases using the given * collection name. If the collection name is null, runs the change stream pipelines directly * on the databases. */ runChangeStreams(dbs, csCollName, csPipelines, csOptions) { const runPipelineFn = (db, collName, pipeline, options) => { const arr = this.runOneAggregation(db, collName, pipeline, options); // Remove fields that we don't expect to be consistent in oplog entries across dbs. return arr.map(doc => { delete doc._id; delete doc.clusterTime; delete doc.wallTime; delete doc.collectionUUID; if (doc.hasOwnProperty('stateBeforeChange') && doc.stateBeforeChange.hasOwnProperty('collectionOptions')) { delete doc.stateBeforeChange.collectionOptions.uuid; } delete doc.lsid; if (doc.hasOwnProperty('operationDescription') && doc.operationDescription.hasOwnProperty('lsid')) { delete doc.operationDescription.lsid; } return doc; }); }; const err = this.runPipelinesAndCompareResults(dbs, csCollName, csPipelines, csOptions, runPipelineFn); if (err) { throw err; } } } const shell$a = globalThis; const STATISTICS_REPORT_FILE_NAME = 'statistics-report.json'; const writeStatisticsFile = statistics => { const path = `${shell$a.pwd()}/${STATISTICS_REPORT_FILE_NAME}`; try { shell$a.removeFile(path); } catch (_) { // pass } shell$a.writeFile(path, JSON.stringify(statistics)); }; function aggMain() { populateVersion(aggGenerated); const compareResults = new AggCompareResults(aggGenerated); const runData = mongoProcessManager.setupFixture({ diffTestingMode: diffTestingMode, optimizationFailPointList: optimizationFailPointList, }); { seedData.createTimeseriesCollections(runData.dbs, collectionNames); } seedData.insertDocs(runData.dbs, collectionNames, documentList); const actualIndexes = seedData.createIndexes({ dbs: runData.dbs, collections: collectionNames, indexes: indexList, indexOptions: indexOptionList, diffTestingMode: diffTestingMode, }); mongoProcessManager.enableDBProfiling(runData.dbs, runData.servers); const controlErr = compareResults.runAggregations(runData.dbs, collectionNames[0], aggregationList, aggregationOptionsList); if (controlErr) { shell$a.print('*********************************'); shell$a.print('The following error & stacktrace is printed manually'); shell$a.print('*********************************'); shell$a.print(controlErr.toString()); shell$a.print(controlErr.stack); shell$a.print('failed to load'); shell$a.print('*********************************'); shell$a.print('Attempting to minimize control error. This process may timeout.'); shell$a.print('*********************************'); // Aggregation minimization: the pipeline and agg options are stored // in the error file, so just grab them const badAggList = [controlErr.cmd]; const badAggListOptions = [controlErr.options]; const cmdFn = () => { // Disable printing explain repeatedly here for every test command run. if (typeof shell$a.disablePrint === 'function') { shell$a.disablePrint(); } const res = compareResults.runAggregations(runData.dbs, collectionNames[0], badAggList, badAggListOptions); if (typeof shell$a.enablePrint === 'function') { shell$a.enablePrint(); } return res; }; // In case of a wildcard fuzzer test, we only create indexes on the experiment db, // so we should only attempt to modify indexes on that db. const dbs = runData.dbs; minimizeAggFuzzerOutputs(dbs, collectionNames, controlErr, cmdFn, actualIndexes, documentList, aggregationList, diffTestingMode !== 'timeseries' ); } else { shell$a.print('No error generated. Minimizer will not run.'); } if (controlErr) { throw controlErr; } shell$a.print('Statistics of the fuzzer run:\n' + compareResults.stats.generateReport()); writeStatisticsFile(compareResults.stats); mongoProcessManager.tearDownFixture(runData.dbs, runData.servers, diffTestingMode); } const shell$b = globalThis; // Only run import statements in the test runner environment. if (typeof process === 'undefined') { { if (typeof shell$b.ReplSetTest === 'undefined') { shell$b.ReplSetTest = await (async () => { const {ReplSetTest} = await import('jstests/libs/replsettest.js'); return ReplSetTest; })(); } if (typeof shell$b.ShardingTest === 'undefined') { shell$b.ShardingTest = await (async () => { const {ShardingTest} = await import('jstests/libs/shardingtest.js'); return ShardingTest; })(); } } } aggMain();