summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/sbe_plan_cache_autoparameterize_collscan.js
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/noPassthrough/sbe_plan_cache_autoparameterize_collscan.js')
-rw-r--r--jstests/noPassthrough/sbe_plan_cache_autoparameterize_collscan.js421
1 files changed, 421 insertions, 0 deletions
diff --git a/jstests/noPassthrough/sbe_plan_cache_autoparameterize_collscan.js b/jstests/noPassthrough/sbe_plan_cache_autoparameterize_collscan.js
new file mode 100644
index 00000000000..13074276090
--- /dev/null
+++ b/jstests/noPassthrough/sbe_plan_cache_autoparameterize_collscan.js
@@ -0,0 +1,421 @@
+/**
+ * Tests that auto-parameterized collection scan plans are correctly stored and in the SBE plan
+ * cache, and that they can be correctly recovered from the cache with new parameter values.
+ *
+ * TODO SERVER-64137: Move this test to jstests/core/ once we no longer need to specially configure
+ * 'featureFlagAutoParameterization'.
+ */
+(function() {
+"use strict";
+
+load("jstests/libs/analyze_plan.js");
+load("jstests/libs/sbe_util.js");
+
+// TODO SERVER-64315: re-enable this test. This test depends on caching single solution plans,
+// which is disabled temporarily due to a bug.
+if (true) {
+ jsTest.log("This test is temporarily disabled");
+ return;
+}
+
+const conn = MongoRunner.runMongod({setParameter: "featureFlagAutoParameterization=true"});
+assert.neq(conn, null, "mongod failed to start up");
+
+const dbName = jsTestName();
+const db = conn.getDB(dbName);
+
+// This test is specifically verifying the behavior of the SBE plan cache. So if either the SBE plan
+// cache or SBE itself are disabled, bail out.
+if (!checkSBEEnabled(db, ["featureFlagSbePlanCache"])) {
+ jsTestLog("Skipping test because either SBE engine or SBE plan cache is disabled");
+ MongoRunner.stopMongod(conn);
+ return;
+}
+
+assert.commandWorked(db.dropDatabase());
+const coll = db.coll;
+
+let data = [
+ {_id: 0, a: 1, c: "foo"},
+ {_id: 1, a: 2, c: "foo"},
+ {_id: 2, a: 3, c: "foo"},
+ {_id: 3, a: 4, c: "foo"},
+ {_id: 4, a: 4, c: "foo"},
+ {_id: 5, a: [3, 4, 5, 6], c: "foo"},
+ {_id: 6, a: [3, 5, 8], c: "foo"},
+ {_id: 7, c: "foo"},
+ {_id: 8, a: [], c: "foo"},
+ {_id: 9, a: undefined, c: "foo"},
+ {_id: 10, a: null, c: "foo"},
+ {_id: 11, a: [{b: 3}, {b: 4}], c: "foo"},
+ {_id: 12, a: [{b: 5}, {b: 6}], c: "foo"},
+ {_id: 13, a: "foo", c: "foo"},
+ {_id: 14, a: /foo/, c: "foo"},
+ {_id: 15, a: "zbarz", c: "foo"},
+ // A 12-byte BinData where the last 6 bits are 1 and all preceding bits are 0.
+ {_id: 16, a: BinData(0, "AAAAAAAAAAAAAAA/"), c: "foo"},
+];
+assert.commandWorked(coll.insert(data));
+
+function assertSbePlanCacheEntryExists(cacheKey) {
+ const entries =
+ coll.aggregate([{$planCacheStats: {}}, {$match: {planCacheKey: cacheKey}}]).toArray();
+ assert.eq(entries.length, 1, entries);
+ const entry = entries[0];
+ // The version:"2" field indicates that this is an SBE plan cache entry.
+ assert.eq(entry.version, "2", entry);
+ assert.eq(entry.planCacheKey, cacheKey, entry);
+ // Since there is only ever one possible candidate plan (collection scan), we expect the cache
+ // entry to be both active and pinned.
+ assert.eq(entry.isActive, true, entry);
+ assert.eq(entry.isPinned, true, entry);
+}
+
+// Given a document with the format {query: <filter>, projection: <projection>, sort: <sort>}, where
+// each field is optional, runs the corresponding find command and returns the results as an array.
+function runFindCommandFromShapeDoc(shape) {
+ let cursor = coll.find(shape.query, shape.projection);
+ if (shape.sort) {
+ cursor = cursor.sort(shape.sort);
+ }
+ return cursor.toArray();
+}
+
+/**
+ * Runs a single end-to-end test case for auto-parameterization of collection scan plans.
+ * - 'shape1' is a description of a find command as a document {query: <filter>, projection:
+ * <projection>, sort: <sort>}.
+ * - 'expectedResults1' is an array containing the results expected from running 'shape1' against
+ * the test collection. This function verifies that the actual results match the expected ones.
+ * The order of the result set is not considered significant (since not all test queries specify a
+ * sort).
+ * - 'shape2' is a second find command, expressed with the same format as 'shape1' and whose
+ * results are compared to `expectedResults2'. Again, the order of the result set is not
+ * significant.
+ * - If 'sameCacheKey' is true, then verifies that 'shape1' and 'shape2' have the same plan cache
+ * key using $planCacheStats. Otherwise, verifies that the two test queries have different plan
+ * cache keys.
+ *
+ * Also uses $planCacheStats to verify that the expected cache entries are created.
+ */
+function runTest(shape1, expectedResults1, shape2, expectedResults2, sameCacheKey) {
+ // Flush the cache before starting the test to make sure we are starting from a clean slate.
+ coll.getPlanCache().clear();
+
+ for (let shape of [shape1, shape2]) {
+ shape.collection = coll;
+ shape.db = db;
+ }
+
+ const cacheKey1 = getPlanCacheKeyFromShape(shape1);
+ const cacheKey2 = getPlanCacheKeyFromShape(shape2);
+ if (sameCacheKey) {
+ assert.eq(cacheKey1, cacheKey2, "expected SBE plan cache keys to be the same");
+ } else {
+ assert.neq(cacheKey1, cacheKey2, "expected SBE plan cache keys to be different");
+ }
+
+ // Run each query twice in order to make sure that each query still returns the same results
+ // after the state of the cache has been altered.
+ [...Array(2)].forEach(() => {
+ const actualResults1 = runFindCommandFromShapeDoc(shape1);
+ assert.sameMembers(actualResults1, expectedResults1, shape1);
+ assertSbePlanCacheEntryExists(cacheKey1);
+
+ const actualResults2 = runFindCommandFromShapeDoc(shape2);
+ assert.sameMembers(actualResults2, expectedResults2, shape2);
+ assertSbePlanCacheEntryExists(cacheKey2);
+ });
+}
+
+// Test basic auto-parameterization of $eq.
+runTest({query: {a: 1}},
+ [{_id: 0, a: 1, c: "foo"}],
+ {query: {a: 4}},
+ [{_id: 3, a: 4, c: "foo"}, {_id: 4, a: 4, c: "foo"}, {_id: 5, a: [3, 4, 5, 6], c: "foo"}],
+ true);
+
+// Test that different projections result in different cache keys.
+runTest({query: {a: 1}, projection: {_id: 0}},
+ [{a: 1, c: "foo"}],
+ {query: {a: 4}, projection: {_id: 0, c: 0}},
+ [{a: 4}, {a: 4}, {a: [3, 4, 5, 6]}],
+ false);
+
+// Test that different sorts result in different cache keys.
+runTest({query: {a: 1}, sort: {_id: -1}, projection: {c: 0}},
+ [{_id: 0, a: 1}],
+ {query: {a: 4}, sort: {_id: 1}, projection: {c: 0}},
+ [{_id: 3, a: 4}, {_id: 4, a: 4}, {_id: 5, a: [3, 4, 5, 6]}],
+ false);
+
+// Queries on different paths should result in different cache keys.
+runTest({query: {a: 1}},
+ [{_id: 0, a: 1, c: "foo"}],
+ {query: {"a.b": 6}},
+ [{_id: 12, a: [{b: 5}, {b: 6}], c: "foo"}],
+ false);
+
+// Test $eq:null queries do not get auto-parameterized.
+runTest({query: {a: 1}, projection: {c: 0}},
+ [{_id: 0, a: 1}],
+ {query: {a: null}, projection: {c: 0}},
+ [{_id: 7}, {_id: 9, a: undefined}, {_id: 10, a: null}],
+ false);
+
+// Test basic auto-parameterization of $lt.
+runTest({query: {a: {$lt: 3}}, projection: {c: 0}},
+ [{_id: 0, a: 1}, {_id: 1, a: 2}],
+ {query: {a: {$lt: 4}}, projection: {c: 0}},
+ [
+ {_id: 0, a: 1},
+ {_id: 1, a: 2},
+ {_id: 2, a: 3},
+ {_id: 5, a: [3, 4, 5, 6]},
+ {_id: 6, a: [3, 5, 8]}
+ ],
+ true);
+
+// Test basic auto-parameterization of $lte.
+runTest({query: {a: {$lte: 2}}, projection: {c: 0}},
+ [{_id: 0, a: 1}, {_id: 1, a: 2}],
+ {query: {a: {$lte: 3}}, projection: {c: 0}},
+ [
+ {_id: 0, a: 1},
+ {_id: 1, a: 2},
+ {_id: 2, a: 3},
+ {_id: 5, a: [3, 4, 5, 6]},
+ {_id: 6, a: [3, 5, 8]}
+ ],
+ true);
+
+// Test basic auto-parameterization of $gt.
+runTest({query: {a: {$gt: 5}}, projection: {c: 0}},
+ [{_id: 5, a: [3, 4, 5, 6]}, {_id: 6, a: [3, 5, 8]}],
+ {query: {a: {$gt: 6}}, projection: {c: 0}},
+ [{_id: 6, a: [3, 5, 8]}],
+ true);
+
+// Test basic auto-parameterization of $gte.
+runTest({query: {a: {$gte: 6}}, projection: {c: 0}},
+ [{_id: 5, a: [3, 4, 5, 6]}, {_id: 6, a: [3, 5, 8]}],
+ {query: {a: {$gte: 7}}, projection: {c: 0}},
+ [{_id: 6, a: [3, 5, 8]}],
+ true);
+
+// Test basic auto-parameterization of $bitsAllClear.
+runTest({query: {a: {$bitsAllClear: [0, 3]}}, projection: {_id: 1}},
+ [{_id: 1}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 16}],
+ {query: {a: {$bitsAllClear: [0, 2, 65]}}, projection: {_id: 1}},
+ [{_id: 1}, {_id: 6}, {_id: 16}],
+ true);
+
+// Test basic auto-parameterization of $bitsAllSet.
+runTest({query: {a: {$bitsAllSet: [0, 2]}}, projection: {_id: 1}},
+ [{_id: 5}, {_id: 6}],
+ {query: {a: {$bitsAllSet: [0, 1]}}, projection: {_id: 1}},
+ [{_id: 2}, {_id: 5}, {_id: 6}],
+ true);
+
+// Test basic auto-parameterization of $bitsAnyClear.
+runTest({query: {a: {$bitsAnyClear: 1}}, projection: {_id: 1}},
+ [{_id: 1}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}, {_id: 16}],
+ {query: {a: {$bitsAnyClear: 3}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}, {_id: 16}],
+ true);
+
+// Test basic auto-parameterization of $bitsAnySet.
+runTest({query: {a: {$bitsAnySet: 1}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 2}, {_id: 5}, {_id: 6}],
+ {query: {a: {$bitsAnySet: 3}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 5}, {_id: 6}],
+ true);
+
+// Auto-parameterization of bit-test operators should work even if looking past 64 bits is required
+// in order to match against binary data.
+runTest({query: {a: {$bitsAllSet: [0, 94]}}, projection: {_id: 1}},
+ [],
+ {query: {a: {$bitsAllSet: [88, 89, 90, 91, 92, 93]}}, projection: {_id: 1}},
+ [{_id: 16}],
+ true);
+
+// Test auto-parameterization of $elemMatch object.
+runTest({query: {a: {$elemMatch: {b: {$gt: 3, $lt: 5}}}}, projection: {_id: 1}},
+ [{_id: 11}],
+ {query: {a: {$elemMatch: {b: {$gt: 4, $lt: 6}}}}, projection: {_id: 1}},
+ [{_id: 12}],
+ true);
+
+// Test a conjunction with two auto-parameterized predicates.
+runTest({query: {$and: [{a: 3}, {a: 6}]}, projection: {_id: 1}},
+ [{_id: 5}],
+ {query: {$and: [{a: 5}, {a: 8}]}, projection: {_id: 1}},
+ [{_id: 6}],
+ true);
+
+// Test a disjunction with two auto-parameterized predicates.
+runTest({query: {$or: [{a: 3}, {a: 6}]}, projection: {_id: 1}},
+ [{_id: 2}, {_id: 5}, {_id: 6}],
+ {query: {$or: [{a: 1}, {a: 4}]}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 3}, {_id: 4}, {_id: 5}],
+ true);
+
+// Test a $nor with three auto-parmeterized child predicates.
+runTest({query: {$nor: [{a: 3}, {a: 6}], a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 3}, {_id: 4}],
+ {query: {$nor: [{a: 1}, {a: 4}], a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 1}, {_id: 2}, {_id: 6}],
+ true);
+
+// Test an auto-parameterized $ne.
+runTest({query: {$and: [{a: {$ne: 4}}, {a: {$type: "number"}}]}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 6}],
+ {query: {$and: [{a: {$ne: 6}}, {a: {$type: "number"}}]}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 6}],
+ true);
+
+// Test an auto-parameterized $not-$lt.
+runTest({query: {$and: [{a: {$not: {$lt: 4}}}, {a: {$type: "number"}}]}, projection: {_id: 1}},
+ [{_id: 3}, {_id: 4}],
+ {query: {$and: [{a: {$not: {$lt: 3}}}, {a: {$type: "number"}}]}, projection: {_id: 1}},
+ [{_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ true);
+
+// Verify that $exists queries are not auto-parameterized, meaning that $exists:true and
+// $exists:false queries get different cache keys.
+runTest({query: {a: {$exists: true}}, projection: {_id: 1}},
+ [
+ {_id: 0},
+ {_id: 1},
+ {_id: 2},
+ {_id: 3},
+ {_id: 4},
+ {_id: 5},
+ {_id: 6},
+ {_id: 8},
+ {_id: 9},
+ {_id: 10},
+ {_id: 11},
+ {_id: 12},
+ {_id: 13},
+ {_id: 14},
+ {_id: 15},
+ {_id: 16},
+ ],
+ {query: {a: {$exists: false}}, projection: {_id: 1}},
+ [{_id: 7}],
+ false);
+
+// Test that comparisons expressed as $expr are not auto-parameterized.
+runTest({query: {$expr: {$eq: ["$a", 3]}}, projection: {_id: 1}},
+ [{_id: 2}],
+ {query: {$expr: {$eq: ["$a", 4]}}, projection: {_id: 1}},
+ [{_id: 3}, {_id: 4}],
+ false);
+runTest({query: {$expr: {$lt: ["$a", 3]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}],
+ {query: {$expr: {$lt: ["$a", 4]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}],
+ false);
+runTest({query: {$expr: {$lte: ["$a", 3]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}],
+ {query: {$expr: {$lte: ["$a", 4]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}],
+ false);
+runTest({query: {$expr: {$gt: ["$a", 2]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ {query: {$expr: {$gt: ["$a", 3]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ false);
+runTest({query: {$expr: {$gte: ["$a", 2]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ {query: {$expr: {$gte: ["$a", 3]}, a: {$type: "number"}}, projection: {_id: 1}},
+ [{_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ false);
+
+// Test that the entire list of $in values is treated as a parameter.
+runTest({query: {a: {$in: [1, 2]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}],
+ {query: {a: {$in: [1, 2, 3, 4]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ true);
+
+// Adding a null value to an $in inhibits auto-parameterization.
+runTest({query: {a: {$in: [1, 2]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}],
+ {query: {a: {$in: [1, 2, null]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 7}, {_id: 9}, {_id: 10}],
+ false);
+
+// Adding a regex to an $in inhibits auto-parameterization.
+runTest({query: {a: {$in: [1, 2]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}],
+ {query: {a: {$in: [1, 2, /foo/]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 13}],
+ false);
+
+// Adding a nested array to an $in inhibits auto-parameterization.
+runTest({query: {a: {$in: [1, 2]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}],
+ {query: {a: {$in: [1, 2, []]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 8}],
+ false);
+
+// Test auto-parameterization of $mod.
+runTest({query: {a: {$mod: [2, 0]}}, projection: {_id: 1}},
+ [{_id: 1}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ {query: {a: {$mod: [3, 1]}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 3}, {_id: 4}, {_id: 5}],
+ true);
+
+// Test auto-parameterization of $size.
+runTest({query: {a: {$size: 4}}, projection: {_id: 1}},
+ [{_id: 5}],
+ {query: {a: {$size: 2}}, projection: {_id: 1}},
+ [{_id: 11}, {_id: 12}],
+ true);
+
+// Test auto-parameterization of $where.
+runTest({query: {$where: "this.a == 1;"}, projection: {_id: 1}},
+ [{_id: 0}],
+ {query: {$where: "this.a == 2;"}, projection: {_id: 1}},
+ [{_id: 1}],
+ true);
+// $where queries use the same plan regardless of the exact JS code.
+runTest({
+ query: {
+ $where: function() {
+ const date = new Date();
+ return this.c == 1;
+ }
+ },
+ projection: {_id: 1}
+},
+ [],
+ {query: {$where: "this.a == 2;"}, projection: {_id: 1}},
+ [{_id: 1}],
+ true);
+
+// Test auto-parameterization of $regex.
+runTest({query: {a: /foo/}, projection: {_id: 1}},
+ [{_id: 13}, {_id: 14}],
+ {query: {a: {$regex: "bar"}}, projection: {_id: 1}},
+ [{_id: 15}],
+ true);
+
+// Test auto-parameterization of $type.
+runTest({query: {a: {$type: "double"}}, projection: {_id: 1}},
+ [{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 6}],
+ {query: {a: {$type: ["string", "regex"]}}, projection: {_id: 1}},
+ [{_id: 13}, {_id: 14}, {_id: 15}],
+ true);
+
+// Test that $type is not auto-parameterized when the type set includes "array".
+runTest({query: {a: {$type: ["string", "regex"]}}, projection: {_id: 1}},
+ [{_id: 13}, {_id: 14}, {_id: 15}],
+ {query: {a: {$type: ["string", "array"]}}, projection: {_id: 1}},
+ [{_id: 5}, {_id: 6}, {_id: 8}, {_id: 11}, {_id: 12}, {_id: 13}, {_id: 15}],
+ false);
+
+MongoRunner.stopMongod(conn);
+}());