Skip to content

Commit 9842c6e

Browse files
stevestencildplewis
andcommitted
adds ability to set hint on Parse.Query #6288 (#6322)
* added hint to aggregate * added support for hint in query * added else clause to aggregate * fixed tests * updated tests * Add tests and clean up * Add support for explain Co-authored-by: Diamond Lewis <[email protected]>
1 parent 5a1d94e commit 9842c6e

File tree

9 files changed

+267
-21
lines changed

9 files changed

+267
-21
lines changed

spec/ParseQuery.hint.spec.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
'use strict';
2+
3+
const Config = require('../lib/Config');
4+
const TestUtils = require('../lib/TestUtils');
5+
const request = require('../lib/request');
6+
7+
let config;
8+
9+
const masterKeyHeaders = {
10+
'X-Parse-Application-Id': 'test',
11+
'X-Parse-Rest-API-Key': 'rest',
12+
'X-Parse-Master-Key': 'test',
13+
'Content-Type': 'application/json',
14+
};
15+
16+
const masterKeyOptions = {
17+
headers: masterKeyHeaders,
18+
json: true,
19+
};
20+
21+
describe_only_db('mongo')('Parse.Query hint', () => {
22+
beforeEach(() => {
23+
config = Config.get('test');
24+
});
25+
26+
afterEach(async () => {
27+
await config.database.schemaCache.clear();
28+
await TestUtils.destroyAllDataPermanently(false);
29+
});
30+
31+
it('query find with hint string', async () => {
32+
const object = new TestObject();
33+
await object.save();
34+
35+
const collection = await config.database.adapter._adaptiveCollection(
36+
'TestObject'
37+
);
38+
let explain = await collection._rawFind(
39+
{ _id: object.id },
40+
{ explain: true }
41+
);
42+
expect(explain.queryPlanner.winningPlan.stage).toBe('IDHACK');
43+
explain = await collection._rawFind(
44+
{ _id: object.id },
45+
{ hint: '_id_', explain: true }
46+
);
47+
expect(explain.queryPlanner.winningPlan.stage).toBe('FETCH');
48+
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
49+
});
50+
51+
it('query find with hint object', async () => {
52+
const object = new TestObject();
53+
await object.save();
54+
55+
const collection = await config.database.adapter._adaptiveCollection(
56+
'TestObject'
57+
);
58+
let explain = await collection._rawFind(
59+
{ _id: object.id },
60+
{ explain: true }
61+
);
62+
expect(explain.queryPlanner.winningPlan.stage).toBe('IDHACK');
63+
explain = await collection._rawFind(
64+
{ _id: object.id },
65+
{ hint: { _id: 1 }, explain: true }
66+
);
67+
expect(explain.queryPlanner.winningPlan.stage).toBe('FETCH');
68+
expect(explain.queryPlanner.winningPlan.inputStage.keyPattern).toEqual({
69+
_id: 1,
70+
});
71+
});
72+
73+
it('query aggregate with hint string', async () => {
74+
const object = new TestObject({ foo: 'bar' });
75+
await object.save();
76+
77+
const collection = await config.database.adapter._adaptiveCollection(
78+
'TestObject'
79+
);
80+
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
81+
explain: true,
82+
});
83+
let { queryPlanner } = result[0].stages[0].$cursor;
84+
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
85+
86+
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
87+
hint: '_id_',
88+
explain: true,
89+
});
90+
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
91+
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
92+
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
93+
});
94+
95+
it('query aggregate with hint object', async () => {
96+
const object = new TestObject({ foo: 'bar' });
97+
await object.save();
98+
99+
const collection = await config.database.adapter._adaptiveCollection(
100+
'TestObject'
101+
);
102+
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
103+
explain: true,
104+
});
105+
let { queryPlanner } = result[0].stages[0].$cursor;
106+
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
107+
108+
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
109+
hint: { _id: 1 },
110+
explain: true,
111+
});
112+
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
113+
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
114+
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
115+
});
116+
117+
it('query find with hint (rest)', async () => {
118+
const object = new TestObject();
119+
await object.save();
120+
let options = Object.assign({}, masterKeyOptions, {
121+
url: Parse.serverURL + '/classes/TestObject',
122+
qs: {
123+
explain: true,
124+
},
125+
});
126+
let response = await request(options);
127+
let explain = response.data.results;
128+
expect(explain.queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
129+
130+
options = Object.assign({}, masterKeyOptions, {
131+
url: Parse.serverURL + '/classes/TestObject',
132+
qs: {
133+
explain: true,
134+
hint: '_id_',
135+
},
136+
});
137+
response = await request(options);
138+
explain = response.data.results;
139+
expect(
140+
explain.queryPlanner.winningPlan.inputStage.inputStage.indexName
141+
).toBe('_id_');
142+
});
143+
144+
it('query aggregate with hint (rest)', async () => {
145+
const object = new TestObject({ foo: 'bar' });
146+
await object.save();
147+
let options = Object.assign({}, masterKeyOptions, {
148+
url: Parse.serverURL + '/aggregate/TestObject',
149+
qs: {
150+
explain: true,
151+
group: JSON.stringify({ objectId: '$foo' }),
152+
},
153+
});
154+
let response = await request(options);
155+
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
156+
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
157+
158+
options = Object.assign({}, masterKeyOptions, {
159+
url: Parse.serverURL + '/aggregate/TestObject',
160+
qs: {
161+
explain: true,
162+
hint: '_id_',
163+
group: JSON.stringify({ objectId: '$foo' }),
164+
},
165+
});
166+
response = await request(options);
167+
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
168+
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
169+
});
170+
});

src/Adapters/Storage/Mongo/MongoCollection.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ export default class MongoCollection {
1313
// none, then build the geoindex.
1414
// This could be improved a lot but it's not clear if that's a good
1515
// idea. Or even if this behavior is a good idea.
16-
find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
16+
find(
17+
query,
18+
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain } = {}
19+
) {
1720
// Support for Full Text Search - $text
1821
if (keys && keys.$score) {
1922
delete keys.$score;
@@ -26,6 +29,8 @@ export default class MongoCollection {
2629
keys,
2730
maxTimeMS,
2831
readPreference,
32+
hint,
33+
explain,
2934
}).catch(error => {
3035
// Check for "no geoindex" error
3136
if (
@@ -54,18 +59,24 @@ export default class MongoCollection {
5459
keys,
5560
maxTimeMS,
5661
readPreference,
62+
hint,
63+
explain,
5764
})
5865
)
5966
);
6067
});
6168
}
6269

63-
_rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
70+
_rawFind(
71+
query,
72+
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain } = {}
73+
) {
6474
let findOperation = this._mongoCollection.find(query, {
6575
skip,
6676
limit,
6777
sort,
6878
readPreference,
79+
hint,
6980
});
7081

7182
if (keys) {
@@ -76,10 +87,10 @@ export default class MongoCollection {
7687
findOperation = findOperation.maxTimeMS(maxTimeMS);
7788
}
7889

79-
return findOperation.toArray();
90+
return explain ? findOperation.explain(explain) : findOperation.toArray();
8091
}
8192

82-
count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) {
93+
count(query, { skip, limit, sort, maxTimeMS, readPreference, hint } = {}) {
8394
// If query is empty, then use estimatedDocumentCount instead.
8495
// This is due to countDocuments performing a scan,
8596
// which greatly increases execution time when being run on large collections.
@@ -96,6 +107,7 @@ export default class MongoCollection {
96107
sort,
97108
maxTimeMS,
98109
readPreference,
110+
hint,
99111
});
100112

101113
return countOperation;
@@ -105,9 +117,9 @@ export default class MongoCollection {
105117
return this._mongoCollection.distinct(field, query);
106118
}
107119

108-
aggregate(pipeline, { maxTimeMS, readPreference } = {}) {
120+
aggregate(pipeline, { maxTimeMS, readPreference, hint, explain } = {}) {
109121
return this._mongoCollection
110-
.aggregate(pipeline, { maxTimeMS, readPreference })
122+
.aggregate(pipeline, { maxTimeMS, readPreference, hint, explain })
111123
.toArray();
112124
}
113125

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ export class MongoStorageAdapter implements StorageAdapter {
620620
className: string,
621621
schema: SchemaType,
622622
query: QueryType,
623-
{ skip, limit, sort, keys, readPreference }: QueryOptions
623+
{ skip, limit, sort, keys, readPreference, hint, explain }: QueryOptions
624624
): Promise<any> {
625625
schema = convertParseSchemaToMongoSchema(schema);
626626
const mongoWhere = transformWhere(className, query, schema);
@@ -652,13 +652,18 @@ export class MongoStorageAdapter implements StorageAdapter {
652652
keys: mongoKeys,
653653
maxTimeMS: this._maxTimeMS,
654654
readPreference,
655+
hint,
656+
explain,
655657
})
656658
)
657-
.then(objects =>
658-
objects.map(object =>
659+
.then(objects => {
660+
if (explain) {
661+
return objects;
662+
}
663+
return objects.map(object =>
659664
mongoObjectToParseObject(className, object, schema)
660-
)
661-
)
665+
);
666+
})
662667
.catch(err => this.handleError(err));
663668
}
664669

@@ -712,7 +717,8 @@ export class MongoStorageAdapter implements StorageAdapter {
712717
className: string,
713718
schema: SchemaType,
714719
query: QueryType,
715-
readPreference: ?string
720+
readPreference: ?string,
721+
hint: ?mixed
716722
) {
717723
schema = convertParseSchemaToMongoSchema(schema);
718724
readPreference = this._parseReadPreference(readPreference);
@@ -721,6 +727,7 @@ export class MongoStorageAdapter implements StorageAdapter {
721727
collection.count(transformWhere(className, query, schema, true), {
722728
maxTimeMS: this._maxTimeMS,
723729
readPreference,
730+
hint,
724731
})
725732
)
726733
.catch(err => this.handleError(err));
@@ -760,7 +767,9 @@ export class MongoStorageAdapter implements StorageAdapter {
760767
className: string,
761768
schema: any,
762769
pipeline: any,
763-
readPreference: ?string
770+
readPreference: ?string,
771+
hint: ?mixed,
772+
explain?: boolean
764773
) {
765774
let isPointerField = false;
766775
pipeline = pipeline.map(stage => {
@@ -791,6 +800,8 @@ export class MongoStorageAdapter implements StorageAdapter {
791800
collection.aggregate(pipeline, {
792801
readPreference,
793802
maxTimeMS: this._maxTimeMS,
803+
hint,
804+
explain,
794805
})
795806
)
796807
.then(results => {

src/Adapters/Storage/StorageAdapter.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type QueryOptions = {
1414
distinct?: boolean,
1515
pipeline?: any,
1616
readPreference?: ?string,
17+
hint?: ?mixed,
18+
explain?: Boolean,
1719
};
1820

1921
export type UpdateQueryOptions = {
@@ -92,7 +94,8 @@ export interface StorageAdapter {
9294
schema: SchemaType,
9395
query: QueryType,
9496
readPreference?: string,
95-
estimate?: boolean
97+
estimate?: boolean,
98+
hint?: mixed
9699
): Promise<number>;
97100
distinct(
98101
className: string,
@@ -104,7 +107,9 @@ export interface StorageAdapter {
104107
className: string,
105108
schema: any,
106109
pipeline: any,
107-
readPreference: ?string
110+
readPreference: ?string,
111+
hint: ?mixed,
112+
explain?: boolean
108113
): Promise<any>;
109114
performInitialization(options: ?any): Promise<void>;
110115

0 commit comments

Comments
 (0)