diff options
author | Alya Berciu <alya.berciu@mongodb.com> | 2022-11-04 12:26:45 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-11-04 13:01:14 +0000 |
commit | ca6499eb6111f89758fcb71b319e8b5d0cd03a20 (patch) | |
tree | 620b426592d3883cfb1d21b6a3e6e8c65aec65fb /jstests | |
parent | f06a898557f951bf48569012c4372e770d1c0267 (diff) | |
download | mongo-ca6499eb6111f89758fcb71b319e8b5d0cd03a20.tar.gz |
SERVER-70436 Handle covered $or null queries with regex
Diffstat (limited to 'jstests')
-rw-r--r-- | jstests/core/cover_null_queries.js | 322 | ||||
-rw-r--r-- | jstests/core/null_query_semantics.js | 164 |
2 files changed, 440 insertions, 46 deletions
diff --git a/jstests/core/cover_null_queries.js b/jstests/core/cover_null_queries.js index 7054a16039d..999ba3440a7 100644 --- a/jstests/core/cover_null_queries.js +++ b/jstests/core/cover_null_queries.js @@ -3,7 +3,9 @@ * @tags: [ * assumes_unsharded_collection, * requires_non_retryable_writes, - * requires_fcv_62, + * requires_fcv_60, + * # This test could produce unexpected explain output if additional indexes are created. + * assumes_no_implicit_index_creation, * ] */ (function() { @@ -58,12 +60,22 @@ function validateStages({cmdObj, expectedStages, isAgg}) { */ function validateFindCmdOutputAndPlan({filter, projection, expectedStages, expectedOutput}) { const cmdObj = {find: coll.getName(), filter: filter, projection: projection}; + + // Compare index output with expected output. if (expectedOutput) { const res = assert.commandWorked(coll.runCommand(cmdObj)); const ouputArray = new DBCommandCursor(coll.getDB(), res).toArray(); assert(arrayEq(expectedOutput, ouputArray), ouputArray); } + + // Validate explain. validateStages({cmdObj, expectedStages}); + + // Verify that we get the same output as we expect without an index. + const noIndexCmdObj = Object.assign(cmdObj, {hint: {$natural: 1}}); + const resNoIndex = assert.commandWorked(coll.runCommand(noIndexCmdObj)); + const noIndexOutArr = new DBCommandCursor(coll.getDB(), resNoIndex).toArray(); + assert(arrayEq(expectedOutput, noIndexOutArr), noIndexOutArr); } /** @@ -72,10 +84,18 @@ function validateFindCmdOutputAndPlan({filter, projection, expectedStages, expec * are present in the plan returned. */ function validateSimpleCountCmdOutputAndPlan({filter, expectedStages, expectedCount}) { + // Compare index output with expected output. const cmdObj = {count: coll.getName(), query: filter}; const res = assert.commandWorked(coll.runCommand(cmdObj)); assert.eq(res.n, expectedCount); + + // Validate explain. validateStages({cmdObj, expectedStages}); + + // Verify that we get the same output with and without an index. + const noIndexCmdObj = Object.assign(cmdObj, {hint: {$natural: 1}}); + const resNoIndex = assert.commandWorked(coll.runCommand(noIndexCmdObj)); + assert.eq(resNoIndex.n, expectedCount); } /** @@ -89,11 +109,33 @@ function validateCountAggCmdOutputAndPlan({filter, expectedStages, expectedCount pipeline: pipeline || [{$match: filter}, {$count: "count"}], cursor: {}, }; + + // Compare index output with expected output. const cmdRes = assert.commandWorked(coll.runCommand(cmdObj)); const countRes = cmdRes.cursor.firstBatch; assert.eq(countRes.length, 1, cmdRes); assert.eq(countRes[0].count, expectedCount, countRes); + + // Validate explain. validateStages({cmdObj, expectedStages, isAgg: true}); + + // Verify that we get the same output as we expect without an index. + const noIndexCmdObj = Object.assign(cmdObj, {hint: {$natural: 1}}); + const resNoIndex = assert.commandWorked(coll.runCommand(noIndexCmdObj)); + const countResNoIndex = resNoIndex.cursor.firstBatch; + assert.eq(countResNoIndex.length, 1, cmdRes); + assert.eq(countResNoIndex[0].count, expectedCount, countRes); +} + +/** + * Same as above, but uses a $group count. + */ +function validateGroupCountAggCmdOutputAndPlan({filter, expectedStages, expectedCount}) { + validateCountAggCmdOutputAndPlan({ + expectedStages, + expectedCount, + pipeline: [{$match: filter}, {$group: {_id: 0, count: {$count: {}}}}] + }); } function getExpectedStagesIndexScanAndFetch(extraStages) { @@ -162,7 +204,7 @@ validateFindCmdOutputAndPlan({ expectedStages: {"IXSCAN": 1, "FETCH": 0, "PROJECTION_COVERED": 1}, }); -// Same as above, but special case for null and empty array predicate. +// We can cover a $in with null and an empty array predicate. validateFindCmdOutputAndPlan({ filter: {a: {$in: [null, []]}}, projection: {_id: 1}, @@ -175,6 +217,21 @@ validateSimpleCountCmdOutputAndPlan({ expectedStages: {"IXSCAN": 1, "FETCH": 0}, }); +// We cannot cover a $in with null and an array predicate. +// TODO SERVER-71058: It should be possible to cover this case and the more general case of matching +// an array on a non-multikey index. +validateFindCmdOutputAndPlan({ + filter: {a: {$in: [null, ["a"]]}}, + projection: {_id: 1}, + expectedOutput: [{_id: 3}, {_id: 4}, {_id: 6}, {_id: 7}], + expectedStages: {"IXSCAN": 1, "FETCH": 1, "PROJECTION_SIMPLE": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {a: {$in: [null, ["a"]]}}, + expectedCount: 4, + expectedStages: {"IXSCAN": 1, "FETCH": 1, "COUNT": 1}, +}); + // Verify that a more complex projection that only relies on the _id field does not need a FETCH. validateFindCmdOutputAndPlan({ filter: {a: null}, @@ -701,4 +758,265 @@ validateCountAggCmdOutputAndPlan({ expectedCount: 4, expectedStages: {"OR": 1, "COUNT_SCAN": 2, "IXSCAN": 0, "FETCH": 0}, }); + +// Validate that we can use the optimization when we have regex without array elements in a $in or +// $or. See SERVER-70436 for more details. +coll.drop(); + +assert.commandWorked(coll.insertMany([ + {_id: 1, a: '123456'}, + {_id: 2, a: '1234567'}, + {_id: 3, a: ' 12345678'}, + {_id: 4, a: '444456'}, + {_id: 5, a: ''}, + {_id: 6, a: null}, + {_id: 7}, +])); + +assert.commandWorked(coll.createIndex({a: 1, _id: 1})); + +// TODO SERVER-70998: Can apply optimization in case without regex; however, we still can't use a +// COUNT_SCAN in this case. +validateFindCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: ""}]}, + projection: {_id: 1}, + expectedOutput: [{_id: 5}, {_id: 6}, {_id: 7}], + expectedStages: {"IXSCAN": 1, "FETCH": 0, "PROJECTION_COVERED": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: ""}]}, + expectedCount: 3, + expectedStages: {"COUNT": 1, "IXSCAN": 1, "FETCH": 0}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: ""}]}, + expectedCount: 3, + expectedStages: {"IXSCAN": 1, "FETCH": 0}, +}); + +// Can still apply optimization when we have regex. +validateFindCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: {$regex: "^$"}}]}, + projection: {_id: 1}, + expectedOutput: [{_id: 5}, {_id: 6}, {_id: 7}], + expectedStages: {"IXSCAN": 1, "FETCH": 0, "PROJECTION_COVERED": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: {$regex: "^$"}}]}, + expectedCount: 3, + expectedStages: {"IXSCAN": 1, "FETCH": 0, "COUNT": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: {$regex: "^$"}}]}, + expectedCount: 3, + expectedStages: {"IXSCAN": 1, "FETCH": 0}, +}); + +// Now test case with a multikey index. We can't leverage the optimization here. +assert.commandWorked(coll.insert({_id: 8, a: [1, 2, 3]})); +assert.commandWorked(coll.insert({_id: 9, a: []})); + +validateFindCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: []}, {a: {$regex: "^$"}}]}, + projection: {_id: 1}, + expectedOutput: [{_id: 5}, {_id: 6}, {_id: 7}, {_id: 9}], + expectedStages: {"IXSCAN": 1, "FETCH": 1, "PROJECTION_SIMPLE": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: []}, {a: {$regex: "^$"}}]}, + expectedCount: 4, + expectedStages: {"COUNT": 1, "IXSCAN": 1, "FETCH": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: []}, {a: {$regex: "^$"}}]}, + expectedCount: 4, + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); + +// We also shouldn't cover queries on multikey indexes where $in includes an array, as we will still +// need a filter after the IXSCAN to correctly return +validateFindCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: []}, {a: [2]}]}, + projection: {_id: 1}, + expectedOutput: [{_id: 6}, {_id: 7}, {_id: 9}], + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: []}, {a: [2]}]}, + expectedCount: 3, + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {$or: [{a: null}, {a: []}, {a: [2]}]}, + expectedCount: 3, + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); + +// Validate that when we have a dotted path, we return the correct results for null queries. +coll.drop(); +assert.commandWorked(coll.insertMany([ + {_id: 1, a: 1}, + {_id: 2, a: null}, + {_id: 3}, + {_id: 4, a: {b: 1}}, + {_id: 5, a: {b: null}}, + {_id: 6, a: {c: 1}}, +])); +assert.commandWorked(coll.createIndex({"a.b": 1, _id: 1})); + +validateFindCmdOutputAndPlan({ + filter: {"a.b": null}, + projection: {_id: 1}, + expectedOutput: [{_id: 1}, {_id: 2}, {_id: 3}, {_id: 5}, {_id: 6}], + expectedStages: {"IXSCAN": 1, "PROJECTION_COVERED": 1, "FETCH": 0}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {"a.b": null}, + expectedCount: 5, + expectedStages: {"OR": 1, "COUNT_SCAN": 2, "IXSCAN": 0, "FETCH": 0}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {"a.b": null}, + expectedCount: 5, + expectedStages: {"OR": 1, "COUNT_SCAN": 2, "IXSCAN": 0, "FETCH": 0}, +}); + +validateFindCmdOutputAndPlan({ + filter: {a: {b: null}}, + projection: {_id: 1}, + expectedOutput: [{_id: 5}], + expectedStages: {"COLLSCAN": 1, "PROJECTION_SIMPLE": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {a: {b: null}}, + expectedCount: 1, + expectedStages: {"COLLSCAN": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {a: {b: null}}, + expectedCount: 1, + expectedStages: {"COLLSCAN": 1}, +}); + +// Still need fetch if we don't have a sufficiently restrictive projection. +validateFindCmdOutputAndPlan({ + filter: {"a.b": null}, + projection: {_id: 1, a: 1}, + expectedOutput: [ + {_id: 1, a: 1}, + {_id: 2, a: null}, + {_id: 3}, + {_id: 5, a: {b: null}}, + {_id: 6, a: {c: 1}}, + ], + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); + +// Make index multikey, and test case where field b is nested in an array. +assert.commandWorked(coll.insertMany([ + {_id: 7, a: [{b: null}]}, + {_id: 8, a: [{b: []}]}, + {_id: 9, a: [{b: [1, 2, 3]}]}, + {_id: 10, a: [{b: 123}]}, + {_id: 11, a: [{c: 123}]}, + {_id: 12, a: []}, + {_id: 13, a: [{}]}, + {_id: 14, a: [1, 2, 3]}, + {_id: 15, a: [{b: 1}, {c: 2}, {b: 3}]}, + {_id: 16, a: [null]}, +])); + +validateFindCmdOutputAndPlan({ + filter: {"a.b": null}, + projection: {_id: 1}, + expectedOutput: [ + {_id: 1}, + {_id: 2}, + {_id: 3}, + {_id: 5}, + {_id: 6}, + {_id: 7}, + {_id: 11}, + {_id: 13}, + {_id: 15} + ], + expectedStages: {"IXSCAN": 1, "PROJECTION_SIMPLE": 1, "FETCH": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {"a.b": null}, + expectedCount: 9, + expectedStages: {"COUNT": 1, "IXSCAN": 1, "FETCH": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {"a.b": null}, + expectedCount: 9, + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); + +validateFindCmdOutputAndPlan({ + filter: {a: {b: null}}, + projection: {_id: 1}, + expectedOutput: [{_id: 5}, {_id: 7}], + expectedStages: { + "COLLSCAN": 1, + "PROJECTION_SIMPLE": 1, + }, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {a: {b: null}}, + expectedCount: 2, + expectedStages: {"COLLSCAN": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {a: {b: null}}, + expectedCount: 2, + expectedStages: {"COLLSCAN": 1}, +}); + +validateFindCmdOutputAndPlan({ + filter: {a: [{b: null}]}, + projection: {_id: 1}, + expectedOutput: [{_id: 7}], + expectedStages: {"COLLSCAN": 1, "PROJECTION_SIMPLE": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {a: [{b: null}]}, + expectedCount: 1, + expectedStages: {"COLLSCAN": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {a: [{b: null}]}, + expectedCount: 1, + expectedStages: {"COLLSCAN": 1}, +}); + +// We still need a FETCH for composite paths, because both {a: [1,2,3]} and {"a.b": null} generate +// null index keys, but the former should not match the predicate below. +validateFindCmdOutputAndPlan({ + filter: {"a.b": {$in: [null, []]}}, + projection: {_id: 1}, + expectedOutput: [ + {_id: 1}, + {_id: 2}, + {_id: 3}, + {_id: 5}, + {_id: 6}, + {_id: 7}, + {_id: 8}, + {_id: 11}, + {_id: 13}, + {_id: 15} + ], + expectedStages: {"IXSCAN": 1, "PROJECTION_SIMPLE": 1, "FETCH": 1}, +}); +validateSimpleCountCmdOutputAndPlan({ + filter: {"a.b": {$in: [null, []]}}, + expectedCount: 10, + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); +validateGroupCountAggCmdOutputAndPlan({ + filter: {"a.b": {$in: [null, []]}}, + expectedCount: 10, + expectedStages: {"IXSCAN": 1, "FETCH": 1}, +}); })(); diff --git a/jstests/core/null_query_semantics.js b/jstests/core/null_query_semantics.js index cf4cc836412..1d84dd0888e 100644 --- a/jstests/core/null_query_semantics.js +++ b/jstests/core/null_query_semantics.js @@ -47,6 +47,9 @@ function testNullSemantics(coll) { [{_id: "a_null", a: null}, {_id: "a_undefined", a: undefined}, {_id: "no_a"}]; assert(resultsEq(expected, noProjectResults), tojson(noProjectResults)); + const count = coll.count({a: {$eq: null}}); + assert.eq(count, expected.length); + const projectResults = coll.find({a: {$eq: null}}, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), tojson(projectResults)); }()); @@ -63,6 +66,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count({a: {$ne: null}}); + assert.eq(count, expected.length); + const projectResults = coll.find({a: {$ne: null}}, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), tojson(projectResults)); }()); @@ -78,6 +84,9 @@ function testNullSemantics(coll) { {_id: "no_a"}, ]; + const count = coll.count(query); + assert.eq(count, expected.length); + assert(resultsEq(noProjectResults, expected), noProjectResults); const projectResults = coll.find(query, projectToOnlyA).toArray(); @@ -95,6 +104,9 @@ function testNullSemantics(coll) { {_id: "no_a"}, ]; + const count = coll.count(query); + assert.eq(count, expected.length); + assert(resultsEq(noProjectResults, expected), noProjectResults); const projectResults = coll.find(query, projectToOnlyA).toArray(); @@ -112,6 +124,9 @@ function testNullSemantics(coll) { {_id: "a_subobject_b_undefined", a: {b: undefined}}, ]; + const count = coll.count(query); + assert.eq(count, expected.length); + assert(resultsEq(noProjectResults, expected), noProjectResults); const projectResults = coll.find(query, projectToOnlyA).toArray(); @@ -132,6 +147,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count(query); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -143,6 +161,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count({a: {$exists: false}}); + assert.eq(count, expected.length); + const projectResults = coll.find({a: {$exists: false}}, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), tojson(projectResults)); }()); @@ -150,17 +171,19 @@ function testNullSemantics(coll) { // Test the semantics of the query {"a.b": {$eq: null}}. (function testDottedEqualsNull() { const noProjectResults = coll.find({"a.b": {$eq: null}}).toArray(); - assert(resultsEq(noProjectResults, - [ - {_id: "a_empty_subobject", a: {}}, - {_id: "a_null", a: null}, - {_id: "a_number", a: 4}, - {_id: "a_subobject_b_null", a: {b: null}}, - {_id: "a_subobject_b_undefined", a: {b: undefined}}, - {_id: "a_undefined", a: undefined}, - {_id: "no_a"} - ]), - tojson(noProjectResults)); + const expected = [ + {_id: "a_empty_subobject", a: {}}, + {_id: "a_null", a: null}, + {_id: "a_number", a: 4}, + {_id: "a_subobject_b_null", a: {b: null}}, + {_id: "a_subobject_b_undefined", a: {b: undefined}}, + {_id: "a_undefined", a: undefined}, + {_id: "no_a"} + ]; + assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + + const count = coll.count({"a.b": {$eq: null}}); + assert.eq(count, expected.length); const projectResults = coll.find({"a.b": {$eq: null}}, projectToOnlyADotB).toArray(); assert(resultsEq(projectResults, @@ -171,8 +194,11 @@ function testNullSemantics(coll) { // Test the semantics of the query {"a.b": {$ne: null}}. (function testDottedNotEqualsNull() { const noProjectResults = coll.find({"a.b": {$ne: null}}).toArray(); - assert(resultsEq(noProjectResults, [{_id: "a_subobject_b_not_null", a: {b: "hi"}}]), - tojson(noProjectResults)); + const expected = [{_id: "a_subobject_b_not_null", a: {b: "hi"}}]; + assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + + const count = coll.count({"a.b": {$ne: null}}); + assert.eq(count, expected.length); const projectResults = coll.find({"a.b": {$ne: null}}, projectToOnlyADotB).toArray(); assert(resultsEq(projectResults, [{a: {b: "hi"}}]), tojson(projectResults)); @@ -189,6 +215,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count({"a.b": {$exists: false}}); + assert.eq(count, expected.length); + const projectResults = coll.find({"a.b": {$exists: false}}, projectToOnlyADotB).toArray(); assert(resultsEq(projectResults, [{}, {a: {}}, {}, {}, {}]), tojson(projectResults)); }()); @@ -208,6 +237,9 @@ function testNullSemantics(coll) { `Expected no results for query ${tojson(elemMatchQuery)}, got ` + tojson(noProjectResults)); + const count = coll.count(elemMatchQuery); + assert.eq(count, 0); + let projectResults = coll.find(elemMatchQuery, projectToOnlyA).toArray(); assert(resultsEq(projectResults, []), `Expected no results for query ${tojson(elemMatchQuery)}, got ` + @@ -256,6 +288,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count({a: {$eq: null}}); + assert.eq(count, expected.length); + const projectResults = coll.find({a: {$eq: null}}, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), tojson(projectResults)); }()); @@ -280,6 +315,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count({a: {$ne: null}}); + assert.eq(count, expected.length); + const projectResults = coll.find({a: {$ne: null}}, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), tojson(projectResults)); }()); @@ -303,6 +341,8 @@ function testNullSemantics(coll) { {_id: "a_value_array_no_nulls", a: [1, "string", 4]}, ]; assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + const count = coll.count({a: {$not: {$gte: null}}}); + assert.eq(count, expected.length); }()); // Test the semantics of the query {a: {$in: [null, <number>]}}. @@ -320,6 +360,9 @@ function testNullSemantics(coll) { assert(resultsEq(noProjectResults, expected), noProjectResults); + const count = coll.count({a: {$in: [null, 75]}}); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -339,6 +382,9 @@ function testNullSemantics(coll) { assert(resultsEq(noProjectResults, expected), noProjectResults); + const count = coll.count({$or: [{a: null}, {a: 75}]}); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -366,6 +412,9 @@ function testNullSemantics(coll) { assert(resultsEq(noProjectResults, expected), noProjectResults); + const count = coll.count({a: {$nin: [null, 75]}}); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -401,6 +450,9 @@ function testNullSemantics(coll) { assert(resultsEq(noProjectResults, expected), noProjectResults); + const count = coll.count({a: {$nin: [null, []]}}); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -426,6 +478,9 @@ function testNullSemantics(coll) { assert(resultsEq(noProjectResults, expected), noProjectResults); + const count = coll.count(query); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -441,6 +496,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expectedEqualToNull), tojson(noProjectResults)); + const count = coll.count({a: {$elemMatch: {$eq: null}}}); + assert.eq(count, expectedEqualToNull.length); + let projectResults = coll.find({a: {$elemMatch: {$eq: null}}}, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expectedEqualToNull)), tojson(projectResults)); @@ -473,25 +531,23 @@ function testNullSemantics(coll) { // value for "b". (function testDottedEqualsNull() { const noProjectResults = coll.find({"a.b": {$eq: null}}).toArray(); - assert( - resultsEq(noProjectResults, - [ - {_id: "a_empty_subobject", a: {}}, - {_id: "a_null", a: null}, - {_id: "a_number", a: 4}, - {_id: "a_subobject_b_null", a: {b: null}}, - {_id: "a_subobject_b_undefined", a: {b: undefined}}, - {_id: "a_undefined", a: undefined}, - {_id: "no_a"}, - { - _id: "a_object_array_all_b_nulls", - a: [{b: null}, {b: undefined}, {b: null}, {}] - }, - {_id: "a_object_array_some_b_nulls", a: [{b: null}, {b: 3}, {b: null}]}, - {_id: "a_object_array_some_b_undefined", a: [{b: undefined}, {b: 3}]}, - {_id: "a_object_array_some_b_missing", a: [{b: 3}, {}]}, - ]), - tojson(noProjectResults)); + const expected = [ + {_id: "a_empty_subobject", a: {}}, + {_id: "a_null", a: null}, + {_id: "a_number", a: 4}, + {_id: "a_subobject_b_null", a: {b: null}}, + {_id: "a_subobject_b_undefined", a: {b: undefined}}, + {_id: "a_undefined", a: undefined}, + {_id: "no_a"}, + {_id: "a_object_array_all_b_nulls", a: [{b: null}, {b: undefined}, {b: null}, {}]}, + {_id: "a_object_array_some_b_nulls", a: [{b: null}, {b: 3}, {b: null}]}, + {_id: "a_object_array_some_b_undefined", a: [{b: undefined}, {b: 3}]}, + {_id: "a_object_array_some_b_missing", a: [{b: 3}, {}]}, + ]; + assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + + const count = coll.count({"a.b": {$eq: null}}); + assert.eq(count, expected.length); const projectResults = coll.find({"a.b": {$eq: null}}, projectToOnlyADotB).toArray(); assert(resultsEq(projectResults, @@ -514,18 +570,20 @@ function testNullSemantics(coll) { // Test the semantics of the query {"a.b": {$ne: null}}. (function testDottedNotEqualsNull() { const noProjectResults = coll.find({"a.b": {$ne: null}}).toArray(); - assert(resultsEq(noProjectResults, - [ - {_id: "a_subobject_b_not_null", a: {b: "hi"}}, - {_id: "a_double_array", a: [[]]}, - {_id: "a_empty_array", a: []}, - {_id: "a_object_array_no_b_nulls", a: [{b: 1}, {b: 3}, {b: "string"}]}, - {_id: "a_value_array_all_nulls", a: [null, null]}, - {_id: "a_value_array_no_nulls", a: [1, "string", 4]}, - {_id: "a_value_array_with_null", a: [1, "string", null, 4]}, - {_id: "a_value_array_with_undefined", a: [1, "string", undefined, 4]} - ]), - tojson(noProjectResults)); + const expected = [ + {_id: "a_subobject_b_not_null", a: {b: "hi"}}, + {_id: "a_double_array", a: [[]]}, + {_id: "a_empty_array", a: []}, + {_id: "a_object_array_no_b_nulls", a: [{b: 1}, {b: 3}, {b: "string"}]}, + {_id: "a_value_array_all_nulls", a: [null, null]}, + {_id: "a_value_array_no_nulls", a: [1, "string", 4]}, + {_id: "a_value_array_with_null", a: [1, "string", null, 4]}, + {_id: "a_value_array_with_undefined", a: [1, "string", undefined, 4]} + ]; + assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + + const count = coll.count({"a.b": {$ne: null}}); + assert.eq(count, expected.length); const projectResults = coll.find({"a.b": {$ne: null}}, projectToOnlyADotB).toArray(); assert(resultsEq(projectResults, @@ -560,6 +618,9 @@ function testNullSemantics(coll) { {_id: "a_object_array_some_b_missing", a: [{b: 3}, {}]}, ]; + const count = coll.count({"a.b": {$in: [null, 75]}}); + assert.eq(count, expected.length); + assert(resultsEq(noProjectResults, expected), noProjectResults); }()); @@ -581,6 +642,9 @@ function testNullSemantics(coll) { {_id: "a_object_array_some_b_missing", a: [{b: 3}, {}]}, ]; + const count = coll.count({$or: [{"a.b": null}, {"a.b": 75}]}); + assert.eq(count, expected.length); + assert(resultsEq(noProjectResults, expected), noProjectResults); }()); @@ -601,6 +665,9 @@ function testNullSemantics(coll) { assert(resultsEq(noProjectResults, expected), noProjectResults); + const count = coll.count({"a.b": {$nin: [null, 75]}}); + assert.eq(count, expected.length); + const projectResults = coll.find(query, projectToOnlyA).toArray(); assert(resultsEq(projectResults, extractAValues(expected)), projectResults); }()); @@ -619,6 +686,9 @@ function testNullSemantics(coll) { {_id: "a_value_array_with_undefined", a: [1, "string", undefined, 4]}, ]; + const count = coll.count({"a.b": {$nin: [null, /^str.*/]}}); + assert.eq(count, expected.length); + assert(resultsEq(noProjectResults, expected), noProjectResults); const projectResults = coll.find(query, projectToOnlyA).toArray(); @@ -631,6 +701,9 @@ function testNullSemantics(coll) { let results = coll.find({"a.b": {$elemMatch: {$eq: null}}}).toArray(); assert(resultsEq(results, []), tojson(results)); + const count = coll.count({"a.b": {$elemMatch: {$eq: null}}}); + assert.eq(count, 0); + results = coll.find({"a.b": {$elemMatch: {$ne: null}}}).toArray(); assert(resultsEq(results, []), tojson(results)); }()); @@ -648,6 +721,9 @@ function testNullSemantics(coll) { ]; assert(resultsEq(noProjectResults, expectedEqualToNull), tojson(noProjectResults)); + const count = coll.count({a: {$elemMatch: {b: {$eq: null}}}}); + assert.eq(count, expectedEqualToNull.length); + let projectResults = coll.find({a: {$elemMatch: {b: {$eq: null}}}}, projectToOnlyADotB).toArray(); assert(resultsEq(projectResults, |