* 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 <findlewis@gmail.com>
This commit is contained in:
committed by
Diamond Lewis
parent
5a1d94ed88
commit
9842c6ee42
170
spec/ParseQuery.hint.spec.js
Normal file
170
spec/ParseQuery.hint.spec.js
Normal file
@@ -0,0 +1,170 @@
|
||||
'use strict';
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const TestUtils = require('../lib/TestUtils');
|
||||
const request = require('../lib/request');
|
||||
|
||||
let config;
|
||||
|
||||
const masterKeyHeaders = {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Rest-API-Key': 'rest',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const masterKeyOptions = {
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
};
|
||||
|
||||
describe_only_db('mongo')('Parse.Query hint', () => {
|
||||
beforeEach(() => {
|
||||
config = Config.get('test');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await config.database.schemaCache.clear();
|
||||
await TestUtils.destroyAllDataPermanently(false);
|
||||
});
|
||||
|
||||
it('query find with hint string', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection(
|
||||
'TestObject'
|
||||
);
|
||||
let explain = await collection._rawFind(
|
||||
{ _id: object.id },
|
||||
{ explain: true }
|
||||
);
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('IDHACK');
|
||||
explain = await collection._rawFind(
|
||||
{ _id: object.id },
|
||||
{ hint: '_id_', explain: true }
|
||||
);
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it('query find with hint object', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection(
|
||||
'TestObject'
|
||||
);
|
||||
let explain = await collection._rawFind(
|
||||
{ _id: object.id },
|
||||
{ explain: true }
|
||||
);
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('IDHACK');
|
||||
explain = await collection._rawFind(
|
||||
{ _id: object.id },
|
||||
{ hint: { _id: 1 }, explain: true }
|
||||
);
|
||||
expect(explain.queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.keyPattern).toEqual({
|
||||
_id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('query aggregate with hint string', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection(
|
||||
'TestObject'
|
||||
);
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: '_id_',
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
|
||||
});
|
||||
|
||||
it('query aggregate with hint object', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
|
||||
const collection = await config.database.adapter._adaptiveCollection(
|
||||
'TestObject'
|
||||
);
|
||||
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
explain: true,
|
||||
});
|
||||
let { queryPlanner } = result[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
|
||||
hint: { _id: 1 },
|
||||
explain: true,
|
||||
});
|
||||
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('FETCH');
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
|
||||
it('query find with hint (rest)', async () => {
|
||||
const object = new TestObject();
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let explain = response.data.results;
|
||||
expect(explain.queryPlanner.winningPlan.inputStage.stage).toBe('COLLSCAN');
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/classes/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
explain = response.data.results;
|
||||
expect(
|
||||
explain.queryPlanner.winningPlan.inputStage.inputStage.indexName
|
||||
).toBe('_id_');
|
||||
});
|
||||
|
||||
it('query aggregate with hint (rest)', async () => {
|
||||
const object = new TestObject({ foo: 'bar' });
|
||||
await object.save();
|
||||
let options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
let response = await request(options);
|
||||
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
|
||||
expect(queryPlanner.winningPlan.stage).toBe('COLLSCAN');
|
||||
|
||||
options = Object.assign({}, masterKeyOptions, {
|
||||
url: Parse.serverURL + '/aggregate/TestObject',
|
||||
qs: {
|
||||
explain: true,
|
||||
hint: '_id_',
|
||||
group: JSON.stringify({ objectId: '$foo' }),
|
||||
},
|
||||
});
|
||||
response = await request(options);
|
||||
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
|
||||
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,10 @@ export default class MongoCollection {
|
||||
// none, then build the geoindex.
|
||||
// This could be improved a lot but it's not clear if that's a good
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
|
||||
find(
|
||||
query,
|
||||
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain } = {}
|
||||
) {
|
||||
// Support for Full Text Search - $text
|
||||
if (keys && keys.$score) {
|
||||
delete keys.$score;
|
||||
@@ -26,6 +29,8 @@ export default class MongoCollection {
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
explain,
|
||||
}).catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (
|
||||
@@ -54,18 +59,24 @@ export default class MongoCollection {
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
explain,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
|
||||
_rawFind(
|
||||
query,
|
||||
{ skip, limit, sort, keys, maxTimeMS, readPreference, hint, explain } = {}
|
||||
) {
|
||||
let findOperation = this._mongoCollection.find(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
readPreference,
|
||||
hint,
|
||||
});
|
||||
|
||||
if (keys) {
|
||||
@@ -76,10 +87,10 @@ export default class MongoCollection {
|
||||
findOperation = findOperation.maxTimeMS(maxTimeMS);
|
||||
}
|
||||
|
||||
return findOperation.toArray();
|
||||
return explain ? findOperation.explain(explain) : findOperation.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) {
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference, hint } = {}) {
|
||||
// If query is empty, then use estimatedDocumentCount instead.
|
||||
// This is due to countDocuments performing a scan,
|
||||
// which greatly increases execution time when being run on large collections.
|
||||
@@ -96,6 +107,7 @@ export default class MongoCollection {
|
||||
sort,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
});
|
||||
|
||||
return countOperation;
|
||||
@@ -105,9 +117,9 @@ export default class MongoCollection {
|
||||
return this._mongoCollection.distinct(field, query);
|
||||
}
|
||||
|
||||
aggregate(pipeline, { maxTimeMS, readPreference } = {}) {
|
||||
aggregate(pipeline, { maxTimeMS, readPreference, hint, explain } = {}) {
|
||||
return this._mongoCollection
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference })
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference, hint, explain })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -620,7 +620,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
{ skip, limit, sort, keys, readPreference }: QueryOptions
|
||||
{ skip, limit, sort, keys, readPreference, hint, explain }: QueryOptions
|
||||
): Promise<any> {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
@@ -652,13 +652,18 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
keys: mongoKeys,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
explain,
|
||||
})
|
||||
)
|
||||
.then(objects =>
|
||||
objects.map(object =>
|
||||
.then(objects => {
|
||||
if (explain) {
|
||||
return objects;
|
||||
}
|
||||
return objects.map(object =>
|
||||
mongoObjectToParseObject(className, object, schema)
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -712,7 +717,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference: ?string
|
||||
readPreference: ?string,
|
||||
hint: ?mixed
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
@@ -721,6 +727,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
collection.count(transformWhere(className, query, schema, true), {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
hint,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
@@ -760,7 +767,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
className: string,
|
||||
schema: any,
|
||||
pipeline: any,
|
||||
readPreference: ?string
|
||||
readPreference: ?string,
|
||||
hint: ?mixed,
|
||||
explain?: boolean
|
||||
) {
|
||||
let isPointerField = false;
|
||||
pipeline = pipeline.map(stage => {
|
||||
@@ -791,6 +800,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
collection.aggregate(pipeline, {
|
||||
readPreference,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
hint,
|
||||
explain,
|
||||
})
|
||||
)
|
||||
.then(results => {
|
||||
|
||||
@@ -14,6 +14,8 @@ export type QueryOptions = {
|
||||
distinct?: boolean,
|
||||
pipeline?: any,
|
||||
readPreference?: ?string,
|
||||
hint?: ?mixed,
|
||||
explain?: Boolean,
|
||||
};
|
||||
|
||||
export type UpdateQueryOptions = {
|
||||
@@ -92,7 +94,8 @@ export interface StorageAdapter {
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference?: string,
|
||||
estimate?: boolean
|
||||
estimate?: boolean,
|
||||
hint?: mixed
|
||||
): Promise<number>;
|
||||
distinct(
|
||||
className: string,
|
||||
@@ -104,7 +107,9 @@ export interface StorageAdapter {
|
||||
className: string,
|
||||
schema: any,
|
||||
pipeline: any,
|
||||
readPreference: ?string
|
||||
readPreference: ?string,
|
||||
hint: ?mixed,
|
||||
explain?: boolean
|
||||
): Promise<any>;
|
||||
performInitialization(options: ?any): Promise<void>;
|
||||
|
||||
|
||||
@@ -1289,13 +1289,14 @@ class DatabaseController {
|
||||
distinct,
|
||||
pipeline,
|
||||
readPreference,
|
||||
hint,
|
||||
explain,
|
||||
}: any = {},
|
||||
auth: any = {},
|
||||
validSchemaController: SchemaController.SchemaController
|
||||
): Promise<any> {
|
||||
const isMaster = acl === undefined;
|
||||
const aclGroup = acl || [];
|
||||
|
||||
op =
|
||||
op ||
|
||||
(typeof query.objectId == 'string' && Object.keys(query).length === 1
|
||||
@@ -1333,7 +1334,15 @@ class DatabaseController {
|
||||
sort.updatedAt = sort._updated_at;
|
||||
delete sort._updated_at;
|
||||
}
|
||||
const queryOptions = { skip, limit, sort, keys, readPreference };
|
||||
const queryOptions = {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
readPreference,
|
||||
hint,
|
||||
explain,
|
||||
};
|
||||
Object.keys(sort).forEach(fieldName => {
|
||||
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
||||
throw new Parse.Error(
|
||||
@@ -1406,7 +1415,9 @@ class DatabaseController {
|
||||
className,
|
||||
schema,
|
||||
query,
|
||||
readPreference
|
||||
readPreference,
|
||||
undefined,
|
||||
hint
|
||||
);
|
||||
}
|
||||
} else if (distinct) {
|
||||
@@ -1428,9 +1439,18 @@ class DatabaseController {
|
||||
className,
|
||||
schema,
|
||||
pipeline,
|
||||
readPreference
|
||||
readPreference,
|
||||
hint,
|
||||
explain
|
||||
);
|
||||
}
|
||||
} else if (explain) {
|
||||
return this.adapter.find(
|
||||
className,
|
||||
schema,
|
||||
query,
|
||||
queryOptions
|
||||
);
|
||||
} else {
|
||||
return this.adapter
|
||||
.find(className, schema, query, queryOptions)
|
||||
|
||||
@@ -118,6 +118,8 @@ function RestQuery(
|
||||
case 'includeAll':
|
||||
this.includeAll = true;
|
||||
break;
|
||||
case 'explain':
|
||||
case 'hint':
|
||||
case 'distinct':
|
||||
case 'pipeline':
|
||||
case 'skip':
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as middleware from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
import UsersRouter from './UsersRouter';
|
||||
|
||||
const BASE_KEYS = ['where', 'distinct', 'pipeline'];
|
||||
const BASE_KEYS = ['where', 'distinct', 'pipeline', 'hint', 'explain'];
|
||||
|
||||
const PIPELINE_KEYS = [
|
||||
'addFields',
|
||||
@@ -46,6 +46,14 @@ export class AggregateRouter extends ClassesRouter {
|
||||
if (body.distinct) {
|
||||
options.distinct = String(body.distinct);
|
||||
}
|
||||
if (body.hint) {
|
||||
options.hint = body.hint;
|
||||
delete body.hint;
|
||||
}
|
||||
if (body.explain) {
|
||||
options.explain = body.explain;
|
||||
delete body.explain;
|
||||
}
|
||||
options.pipeline = AggregateRouter.getPipeline(body);
|
||||
if (typeof body.where === 'string') {
|
||||
body.where = JSON.parse(body.where);
|
||||
@@ -96,7 +104,6 @@ export class AggregateRouter extends ClassesRouter {
|
||||
*/
|
||||
static getPipeline(body) {
|
||||
let pipeline = body.pipeline || body;
|
||||
|
||||
if (!Array.isArray(pipeline)) {
|
||||
pipeline = Object.keys(pipeline).map(key => {
|
||||
return { [key]: pipeline[key] };
|
||||
|
||||
@@ -173,6 +173,8 @@ export class ClassesRouter extends PromiseRouter {
|
||||
'readPreference',
|
||||
'includeReadPreference',
|
||||
'subqueryReadPreference',
|
||||
'hint',
|
||||
'explain',
|
||||
];
|
||||
|
||||
for (const key of Object.keys(body)) {
|
||||
@@ -219,6 +221,15 @@ export class ClassesRouter extends PromiseRouter {
|
||||
if (typeof body.subqueryReadPreference === 'string') {
|
||||
options.subqueryReadPreference = body.subqueryReadPreference;
|
||||
}
|
||||
if (
|
||||
body.hint &&
|
||||
(typeof body.hint === 'string' || typeof body.hint === 'object')
|
||||
) {
|
||||
options.hint = body.hint;
|
||||
}
|
||||
if (body.explain) {
|
||||
options.explain = body.explain;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
@@ -499,6 +499,10 @@ export function maybeRunQueryTrigger(
|
||||
restOptions = restOptions || {};
|
||||
restOptions.excludeKeys = jsonQuery.excludeKeys;
|
||||
}
|
||||
if (jsonQuery.explain) {
|
||||
restOptions = restOptions || {};
|
||||
restOptions.explain = jsonQuery.explain;
|
||||
}
|
||||
if (jsonQuery.keys) {
|
||||
restOptions = restOptions || {};
|
||||
restOptions.keys = jsonQuery.keys;
|
||||
@@ -507,6 +511,10 @@ export function maybeRunQueryTrigger(
|
||||
restOptions = restOptions || {};
|
||||
restOptions.order = jsonQuery.order;
|
||||
}
|
||||
if (jsonQuery.hint) {
|
||||
restOptions = restOptions || {};
|
||||
restOptions.hint = jsonQuery.hint;
|
||||
}
|
||||
if (requestObject.readPreference) {
|
||||
restOptions = restOptions || {};
|
||||
restOptions.readPreference = requestObject.readPreference;
|
||||
|
||||
Reference in New Issue
Block a user