summaryrefslogtreecommitdiff
path: root/jstests/core/index_filter_commands.js
blob: 58f78d0514e79f17d2488b7170376babc17c0af3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
 * Index Filter commands
 *
 * Commands:
 * - planCacheListFilters
 *   Displays index filters for all query shapes in a collection.
 *
 * - planCacheClearFilters
 *   Clears index filter for a single query shape or, if the query shape is omitted, all filters for
 *   the collection.
 *
 * - planCacheSetFilter
 *   Sets index filter for a query shape. Overrides existing filter.
 *
 * Not a lot of data access in this test suite. Hint commands manage a non-persistent mapping in the
 * server of query shape to list of index specs.
 *
 * Only time we might need to execute a query is to check the plan cache state. We would do this
 * with the planCacheListPlans command on the same query shape with the index filters.
 *
 * @tags: [
 *   # This test attempts to perform queries with plan cache filters set up. The former operation
 *   # may be routed to a secondary in the replica set, whereas the latter must be routed to the
 *   # primary.
 *   assumes_read_preference_unchanged,
 * ]
 */

load("jstests/libs/analyze_plan.js");

var t = db.jstests_index_filter_commands;

t.drop();

// Setup the data so that plans will not tie given the indices and query
// below. Tying plans will not be cached, and we need cached shapes in
// order to test the filter functionality.
t.save({a: 1});
t.save({a: 1});
t.save({a: 1, b: 1});
t.save({_id: 1});

// Add 2 indexes.
// 1st index is more efficient.
// 2nd and 3rd indexes will be used to test index filters.
var indexA1 = {a: 1};
var indexA1B1 = {a: 1, b: 1};
var indexA1C1 = {a: 1, c: 1};
t.ensureIndex(indexA1);
t.ensureIndex(indexA1B1);
t.ensureIndex(indexA1C1);

var queryAA = {a: "A"};
var queryA1 = {a: 1, b: 1};
var projectionA1 = {_id: 0, a: 1};
var sortA1 = {a: -1};
var queryID = {_id: 1};

//
// Tests for planCacheListFilters, planCacheClearFilters, planCacheSetFilter
//

// Utility function to list index filters.
function getFilters(collection) {
    if (collection == undefined) {
        collection = t;
    }
    var res = collection.runCommand('planCacheListFilters');
    print('planCacheListFilters() = ' + tojson(res));
    assert.commandWorked(res, 'planCacheListFilters failed');
    assert(res.hasOwnProperty('filters'), 'filters missing from planCacheListFilters result');
    return res.filters;
}

// If query shape is in plan cache,
// planCacheListPlans returns non-empty array of plans.
function planCacheContains(shape) {
    var res = t.runCommand('planCacheListPlans', shape);
    assert.commandWorked(res);
    return res.plans.length > 0;
}

// Utility function to list plans for a query.
function getPlans(shape) {
    var res = t.runCommand('planCacheListPlans', shape);
    assert.commandWorked(res, 'planCacheListPlans(' + tojson(shape, '', true) + ' failed');
    assert(res.hasOwnProperty('plans'),
           'plans missing from planCacheListPlans(' + tojson(shape, '', true) + ') result');
    return res.plans;
}

// Attempting to retrieve index filters on a non-existent collection
// will return empty results.
var missingCollection = db.jstests_index_filter_commands_missing;
missingCollection.drop();
assert.eq(0,
          getFilters(missingCollection),
          'planCacheListFilters should return empty array on non-existent collection');

// Retrieve index filters from an empty test collection.
var filters = getFilters();
assert.eq(0, filters.length, 'unexpected number of index filters in planCacheListFilters result');

// Check details of winning plan in plan cache before setting index filter.
assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'unexpected document count');
var shape = {query: queryA1, sort: sortA1, projection: projectionA1};
var planBeforeSetFilter = getPlans(shape)[0];
print('Winning plan (before setting index filters) = ' + tojson(planBeforeSetFilter));
// Check filterSet field in plan details
assert.eq(
    false, planBeforeSetFilter.filterSet, 'missing or invalid filterSet field in plan details');

// Adding index filters to a non-existent collection should be an error.
assert.commandFailed(missingCollection.runCommand(
    'planCacheSetFilter',
    {query: queryA1, sort: sortA1, projection: projectionA1, indexes: [indexA1B1, indexA1C1]}));

// Add index filters for simple query.
assert.commandWorked(t.runCommand(
    'planCacheSetFilter',
    {query: queryA1, sort: sortA1, projection: projectionA1, indexes: [indexA1B1, indexA1C1]}));
filters = getFilters();
assert.eq(
    1, filters.length, 'no change in query settings after successfully setting index filters');
assert.eq(queryA1, filters[0].query, 'unexpected query in filters');
assert.eq(sortA1, filters[0].sort, 'unexpected sort in filters');
assert.eq(projectionA1, filters[0].projection, 'unexpected projection in filters');
assert.eq(2, filters[0].indexes.length, 'unexpected number of indexes in filters');
assert.eq(indexA1B1, filters[0].indexes[0], 'unexpected first index');
assert.eq(indexA1C1, filters[0].indexes[1], 'unexpected first index');

// Plans for query shape should be removed after setting index filter.
assert(!planCacheContains(shape), 'plan cache for query shape not flushed after updating filter');

// Check details of winning plan in plan cache after setting filter and re-executing query.
assert.eq(1, t.find(queryA1, projectionA1).sort(sortA1).itcount(), 'unexpected document count');
planAfterSetFilter = getPlans(shape)[0];
print('Winning plan (after setting index filter) = ' + tojson(planAfterSetFilter));
// Check filterSet field in plan details
assert.eq(true, planAfterSetFilter.filterSet, 'missing or invalid filterSet field in plan details');

// Execute query with cursor.hint(). Check that user-provided hint is overridden.
// Applying the index filters will remove the user requested index from the list
// of indexes provided to the planner.
// If the planner still tries to use the user hint, we will get a 'bad hint' error.
t.find(queryA1, projectionA1).sort(sortA1).hint(indexA1).itcount();

// Test that index filters are ignored for idhack queries.
assert.commandWorked(t.runCommand('planCacheSetFilter', {query: queryID, indexes: [indexA1]}));
var explain = t.explain("executionStats").find(queryID).finish();
assert.commandWorked(explain);
var planStage = getPlanStage(explain.executionStats.executionStages, 'IDHACK');
assert.neq(null, planStage);

// Clear filters
// Clearing filters on a missing collection should be a no-op.
assert.commandWorked(missingCollection.runCommand('planCacheClearFilters'));
// Clear the filters set earlier.
assert.commandWorked(t.runCommand('planCacheClearFilters'));
filters = getFilters();
assert.eq(0, filters.length, 'filters not cleared after successful planCacheClearFilters command');

// Plans should be removed after clearing filters
assert(!planCacheContains(shape), 'plan cache for query shape not flushed after clearing filters');

print('Plan details before setting filter = ' + tojson(planBeforeSetFilter.details, '', true));
print('Plan details after setting filter = ' + tojson(planAfterSetFilter.details, '', true));

//
// Tests for the 'indexFilterSet' explain field.
//

if (db.isMaster().msg !== "isdbgrid") {
    // No filter.
    t.getPlanCache().clear();
    assert.eq(false, t.find({z: 1}).explain('queryPlanner').queryPlanner.indexFilterSet);
    assert.eq(false,
              t.find(queryA1, projectionA1)
                  .sort(sortA1)
                  .explain('queryPlanner')
                  .queryPlanner.indexFilterSet);

    // With one filter set.
    assert.commandWorked(t.runCommand('planCacheSetFilter', {query: {z: 1}, indexes: [{z: 1}]}));
    assert.eq(true, t.find({z: 1}).explain('queryPlanner').queryPlanner.indexFilterSet);
    assert.eq(false,
              t.find(queryA1, projectionA1)
                  .sort(sortA1)
                  .explain('queryPlanner')
                  .queryPlanner.indexFilterSet);

    // With two filters set.
    assert.commandWorked(t.runCommand(
        'planCacheSetFilter',
        {query: queryA1, projection: projectionA1, sort: sortA1, indexes: [indexA1B1, indexA1C1]}));
    assert.eq(true, t.find({z: 1}).explain('queryPlanner').queryPlanner.indexFilterSet);
    assert.eq(true,
              t.find(queryA1, projectionA1)
                  .sort(sortA1)
                  .explain('queryPlanner')
                  .queryPlanner.indexFilterSet);
}

//
// Tests for index filter commands and multiple indexes with the same key pattern.
//

t.drop();

var collationEN = {locale: "en_US"};
assert.commandWorked(t.createIndex(indexA1, {collation: collationEN, name: "a_1:en_US"}));
assert.commandWorked(t.createIndex(indexA1, {name: "a_1"}));

assert.writeOK(t.insert({a: "a"}));

assert.commandWorked(t.runCommand('planCacheSetFilter', {query: queryAA, indexes: [indexA1]}));

assert.commandWorked(t.runCommand('planCacheSetFilter',
                                  {query: queryAA, collation: collationEN, indexes: [indexA1]}));

// Ensure that index key patterns in planCacheSetFilter select any index with a matching key
// pattern.

explain = t.find(queryAA).explain();
assert(isIxscan(explain.queryPlanner.winningPlan), "Expected index scan: " + tojson(explain));

explain = t.find(queryAA).collation(collationEN).explain();
assert(isIxscan(explain.queryPlanner.winningPlan), "Expected index scan: " + tojson(explain));

// Ensure that index names in planCacheSetFilter only select matching names.

assert.commandWorked(
    t.runCommand('planCacheSetFilter', {query: queryAA, collation: collationEN, indexes: ["a_1"]}));

explain = t.find(queryAA).collation(collationEN).explain();
assert(isCollscan(explain.queryPlanner.winningPlan), "Expected collscan: " + tojson(explain));