Improved match aggregate (#4495)

* Improve aggregate queries

* using pg-promise query formatting

* match multiple comparison

* $or and complex match
This commit is contained in:
Diamond Lewis
2018-01-20 08:00:36 -06:00
committed by GitHub
parent 33890bbbfc
commit 64e568d000
3 changed files with 206 additions and 28 deletions

View File

@@ -14,10 +14,10 @@ const masterKeyOptions = {
} }
const loadTestData = () => { const loadTestData = () => {
const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['S', 'M']}; const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 900, size: ['S', 'M']};
const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['M', 'L']}; const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 800, size: ['M', 'L']};
const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, size: ['S']}; const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, views: 700, size: ['S']};
const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, size: ['S']}; const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, views: 700, size: ['S']};
const obj1 = new TestObject(data1); const obj1 = new TestObject(data1);
const obj2 = new TestObject(data2); const obj2 = new TestObject(data2);
const obj3 = new TestObject(data3); const obj3 = new TestObject(data3);
@@ -252,7 +252,7 @@ describe('Parse.Query Aggregate testing', () => {
}).catch(done.fail); }).catch(done.fail);
}); });
it('match query', (done) => { it('match comparison query', (done) => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {
match: { score: { $gt: 15 }}, match: { score: { $gt: 15 }},
@@ -266,6 +266,127 @@ describe('Parse.Query Aggregate testing', () => {
}).catch(done.fail); }).catch(done.fail);
}); });
it('match multiple comparison query', (done) => {
const options = Object.assign({}, masterKeyOptions, {
body: {
match: { score: { $gt: 5, $lt: 15 }},
}
});
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
.then((resp) => {
expect(resp.results.length).toBe(3);
expect(resp.results[0].score).toBe(10);
expect(resp.results[1].score).toBe(10);
expect(resp.results[2].score).toBe(10);
done();
}).catch(done.fail);
});
it('match complex comparison query', (done) => {
const options = Object.assign({}, masterKeyOptions, {
body: {
match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 }},
}
});
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
.then((resp) => {
expect(resp.results.length).toBe(1);
expect(resp.results[0].score).toBe(10);
expect(resp.results[0].views).toBe(900);
done();
}).catch(done.fail);
});
it('match comparison and equality query', (done) => {
const options = Object.assign({}, masterKeyOptions, {
body: {
match: { score: { $gt: 5, $lt: 15 }, views: 900},
}
});
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
.then((resp) => {
expect(resp.results.length).toBe(1);
expect(resp.results[0].score).toBe(10);
expect(resp.results[0].views).toBe(900);
done();
}).catch(done.fail);
});
it('match $or query', (done) => {
const options = Object.assign({}, masterKeyOptions, {
body: {
match: { $or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }]},
}
});
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
.then((resp) => {
expect(resp.results.length).toBe(2);
// Match score { $gt: 15, $lt: 25 }
expect(resp.results.some(result => result.score === 20)).toEqual(true);
expect(resp.results.some(result => result.views === 700)).toEqual(true);
// Match view { $gt: 750, $lt: 850 }
expect(resp.results.some(result => result.score === 10)).toEqual(true);
expect(resp.results.some(result => result.views === 800)).toEqual(true);
done();
}).catch(done.fail);
});
it('match objectId query', (done) => {
const obj1 = new TestObject();
const obj2 = new TestObject();
Parse.Object.saveAll([obj1, obj2]).then(() => {
const pipeline = [
{ match: { objectId: obj1.id } }
];
const query = new Parse.Query(TestObject);
return query.aggregate(pipeline);
}).then((results) => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(obj1.id);
done();
});
});
it('match field query', (done) => {
const obj1 = new TestObject({ name: 'TestObject1'});
const obj2 = new TestObject({ name: 'TestObject2'});
Parse.Object.saveAll([obj1, obj2]).then(() => {
const pipeline = [
{ match: { name: 'TestObject1' } }
];
const query = new Parse.Query(TestObject);
return query.aggregate(pipeline);
}).then((results) => {
expect(results.length).toEqual(1);
expect(results[0].objectId).toEqual(obj1.id);
done();
});
});
it('match pointer query', (done) => {
const pointer1 = new TestObject();
const pointer2 = new TestObject();
const obj1 = new TestObject({ pointer: pointer1 });
const obj2 = new TestObject({ pointer: pointer2 });
const obj3 = new TestObject({ pointer: pointer1 });
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]).then(() => {
const pipeline = [
{ match: { pointer: pointer1.id } }
];
const query = new Parse.Query(TestObject);
return query.aggregate(pipeline);
}).then((results) => {
expect(results.length).toEqual(2);
expect(results[0].pointer.objectId).toEqual(pointer1.id);
expect(results[1].pointer.objectId).toEqual(pointer1.id);
expect(results.some(result => result.objectId === obj1.id)).toEqual(true);
expect(results.some(result => result.objectId === obj3.id)).toEqual(true);
done();
});
});
it('project query', (done) => { it('project query', (done) => {
const options = Object.assign({}, masterKeyOptions, { const options = Object.assign({}, masterKeyOptions, {
body: { body: {

View File

@@ -524,6 +524,20 @@ export class MongoStorageAdapter implements StorageAdapter {
stage.$group._id = `$_p_${field}`; stage.$group._id = `$_p_${field}`;
} }
} }
if (stage.$match) {
for (const field in stage.$match) {
if (schema.fields[field] && schema.fields[field].type === 'Pointer') {
const transformMatch = { [`_p_${field}`] : `${className}$${stage.$match[field]}` };
stage.$match = transformMatch;
}
if (field === 'objectId') {
const transformMatch = Object.assign({}, stage.$match);
transformMatch._id = stage.$match[field];
delete transformMatch.objectId;
stage.$match = transformMatch;
}
}
}
return stage; return stage;
}); });
readPreference = this._parseReadPreference(readPreference); readPreference = this._parseReadPreference(readPreference);

View File

@@ -1507,6 +1507,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
aggregate(className: string, schema: any, pipeline: any) { aggregate(className: string, schema: any, pipeline: any) {
debug('aggregate', className, pipeline); debug('aggregate', className, pipeline);
const values = [className]; const values = [className];
let index = 2;
let columns: string[] = []; let columns: string[] = [];
let countField = null; let countField = null;
let wherePattern = ''; let wherePattern = '';
@@ -1523,26 +1524,38 @@ export class PostgresStorageAdapter implements StorageAdapter {
continue; continue;
} }
if (field === '_id') { if (field === '_id') {
columns.push(`${transformAggregateField(value)} AS "objectId"`); columns.push(`$${index}:name AS "objectId"`);
groupPattern = `GROUP BY ${transformAggregateField(value)}`; groupPattern = `GROUP BY $${index}:name`;
values.push(transformAggregateField(value));
index += 1;
continue; continue;
} }
if (value.$sum) { if (value.$sum) {
if (typeof value.$sum === 'string') { if (typeof value.$sum === 'string') {
columns.push(`SUM(${transformAggregateField(value.$sum)}) AS "${field}"`); columns.push(`SUM($${index}:name) AS $${index + 1}:name`);
values.push(transformAggregateField(value.$sum), field);
index += 2;
} else { } else {
countField = field; countField = field;
columns.push(`COUNT(*) AS "${field}"`); columns.push(`COUNT(*) AS $${index}:name`);
values.push(field);
index += 1;
} }
} }
if (value.$max) { if (value.$max) {
columns.push(`MAX(${transformAggregateField(value.$max)}) AS "${field}"`); columns.push(`MAX($${index}:name) AS $${index + 1}:name`);
values.push(transformAggregateField(value.$max), field);
index += 2;
} }
if (value.$min) { if (value.$min) {
columns.push(`MIN(${transformAggregateField(value.$min)}) AS "${field}"`); columns.push(`MIN($${index}:name) AS $${index + 1}:name`);
values.push(transformAggregateField(value.$min), field);
index += 2;
} }
if (value.$avg) { if (value.$avg) {
columns.push(`AVG(${transformAggregateField(value.$avg)}) AS "${field}"`); columns.push(`AVG($${index}:name) AS $${index + 1}:name`);
values.push(transformAggregateField(value.$avg), field);
index += 2;
} }
} }
} else { } else {
@@ -1555,38 +1568,68 @@ export class PostgresStorageAdapter implements StorageAdapter {
for (const field in stage.$project) { for (const field in stage.$project) {
const value = stage.$project[field]; const value = stage.$project[field];
if ((value === 1 || value === true)) { if ((value === 1 || value === true)) {
columns.push(field); columns.push(`$${index}:name`);
values.push(field);
index += 1;
} }
} }
} }
if (stage.$match) { if (stage.$match) {
const patterns = []; const patterns = [];
for (const field in stage.$match) { const orOrAnd = stage.$match.hasOwnProperty('$or') ? ' OR ' : ' AND ';
const value = stage.$match[field];
Object.keys(ParseToPosgresComparator).forEach(cmp => { if (stage.$match.$or) {
if (value[cmp]) { const collapse = {};
const pgComparator = ParseToPosgresComparator[cmp]; stage.$match.$or.forEach((element) => {
patterns.push(`${field} ${pgComparator} ${value[cmp]}`); for (const key in element) {
collapse[key] = element[key];
} }
}); });
stage.$match = collapse;
} }
wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(' ')}` : ''; for (const field in stage.$match) {
const value = stage.$match[field];
const matchPatterns = [];
Object.keys(ParseToPosgresComparator).forEach((cmp) => {
if (value[cmp]) {
const pgComparator = ParseToPosgresComparator[cmp];
matchPatterns.push(`$${index}:name ${pgComparator} $${index + 1}`);
values.push(field, toPostgresValue(value[cmp]));
index += 2;
}
});
if (matchPatterns.length > 0) {
patterns.push(`(${matchPatterns.join(' AND ')})`);
}
if (schema.fields[field] && schema.fields[field].type && matchPatterns.length === 0) {
patterns.push(`$${index}:name = $${index + 1}`);
values.push(field, value);
index += 2;
}
}
wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(` ${orOrAnd} `)}` : '';
} }
if (stage.$limit) { if (stage.$limit) {
limitPattern = `LIMIT ${stage.$limit}`; limitPattern = `LIMIT $${index}`;
values.push(stage.$limit);
index += 1;
} }
if (stage.$skip) { if (stage.$skip) {
skipPattern = `OFFSET ${stage.$skip}`; skipPattern = `OFFSET $${index}`;
values.push(stage.$skip);
index += 1;
} }
if (stage.$sort) { if (stage.$sort) {
const sort = stage.$sort; const sort = stage.$sort;
const sorting = Object.keys(sort).map((key) => { const keys = Object.keys(sort);
if (sort[key] === 1) { const sorting = keys.map((key) => {
return `"${key}" ASC`; const transformer = sort[key] === 1 ? 'ASC' : 'DESC';
} const order = `$${index}:name ${transformer}`;
return `"${key}" DESC`; index += 1;
return order;
}).join(); }).join();
sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; values.push(...keys);
sortPattern = sort !== undefined && sorting.length > 0 ? `ORDER BY ${sorting}` : '';
} }
} }