Skip to content

Commit 64e568d

Browse files
authored
Improved match aggregate (#4495)
* Improve aggregate queries * using pg-promise query formatting * match multiple comparison * $or and complex match
1 parent 33890bb commit 64e568d

File tree

3 files changed

+202
-24
lines changed

3 files changed

+202
-24
lines changed

spec/ParseQuery.Aggregate.spec.js

Lines changed: 126 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ const masterKeyOptions = {
1414
}
1515

1616
const loadTestData = () => {
17-
const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['S', 'M']};
18-
const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, size: ['M', 'L']};
19-
const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, size: ['S']};
20-
const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, size: ['S']};
17+
const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 900, size: ['S', 'M']};
18+
const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 800, size: ['M', 'L']};
19+
const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, views: 700, size: ['S']};
20+
const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, views: 700, size: ['S']};
2121
const obj1 = new TestObject(data1);
2222
const obj2 = new TestObject(data2);
2323
const obj3 = new TestObject(data3);
@@ -252,7 +252,7 @@ describe('Parse.Query Aggregate testing', () => {
252252
}).catch(done.fail);
253253
});
254254

255-
it('match query', (done) => {
255+
it('match comparison query', (done) => {
256256
const options = Object.assign({}, masterKeyOptions, {
257257
body: {
258258
match: { score: { $gt: 15 }},
@@ -266,6 +266,127 @@ describe('Parse.Query Aggregate testing', () => {
266266
}).catch(done.fail);
267267
});
268268

269+
it('match multiple comparison query', (done) => {
270+
const options = Object.assign({}, masterKeyOptions, {
271+
body: {
272+
match: { score: { $gt: 5, $lt: 15 }},
273+
}
274+
});
275+
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
276+
.then((resp) => {
277+
expect(resp.results.length).toBe(3);
278+
expect(resp.results[0].score).toBe(10);
279+
expect(resp.results[1].score).toBe(10);
280+
expect(resp.results[2].score).toBe(10);
281+
done();
282+
}).catch(done.fail);
283+
});
284+
285+
it('match complex comparison query', (done) => {
286+
const options = Object.assign({}, masterKeyOptions, {
287+
body: {
288+
match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 }},
289+
}
290+
});
291+
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
292+
.then((resp) => {
293+
expect(resp.results.length).toBe(1);
294+
expect(resp.results[0].score).toBe(10);
295+
expect(resp.results[0].views).toBe(900);
296+
done();
297+
}).catch(done.fail);
298+
});
299+
300+
it('match comparison and equality query', (done) => {
301+
const options = Object.assign({}, masterKeyOptions, {
302+
body: {
303+
match: { score: { $gt: 5, $lt: 15 }, views: 900},
304+
}
305+
});
306+
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
307+
.then((resp) => {
308+
expect(resp.results.length).toBe(1);
309+
expect(resp.results[0].score).toBe(10);
310+
expect(resp.results[0].views).toBe(900);
311+
done();
312+
}).catch(done.fail);
313+
});
314+
315+
it('match $or query', (done) => {
316+
const options = Object.assign({}, masterKeyOptions, {
317+
body: {
318+
match: { $or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }]},
319+
}
320+
});
321+
rp.get(Parse.serverURL + '/aggregate/TestObject', options)
322+
.then((resp) => {
323+
expect(resp.results.length).toBe(2);
324+
// Match score { $gt: 15, $lt: 25 }
325+
expect(resp.results.some(result => result.score === 20)).toEqual(true);
326+
expect(resp.results.some(result => result.views === 700)).toEqual(true);
327+
328+
// Match view { $gt: 750, $lt: 850 }
329+
expect(resp.results.some(result => result.score === 10)).toEqual(true);
330+
expect(resp.results.some(result => result.views === 800)).toEqual(true);
331+
done();
332+
}).catch(done.fail);
333+
});
334+
335+
it('match objectId query', (done) => {
336+
const obj1 = new TestObject();
337+
const obj2 = new TestObject();
338+
Parse.Object.saveAll([obj1, obj2]).then(() => {
339+
const pipeline = [
340+
{ match: { objectId: obj1.id } }
341+
];
342+
const query = new Parse.Query(TestObject);
343+
return query.aggregate(pipeline);
344+
}).then((results) => {
345+
expect(results.length).toEqual(1);
346+
expect(results[0].objectId).toEqual(obj1.id);
347+
done();
348+
});
349+
});
350+
351+
it('match field query', (done) => {
352+
const obj1 = new TestObject({ name: 'TestObject1'});
353+
const obj2 = new TestObject({ name: 'TestObject2'});
354+
Parse.Object.saveAll([obj1, obj2]).then(() => {
355+
const pipeline = [
356+
{ match: { name: 'TestObject1' } }
357+
];
358+
const query = new Parse.Query(TestObject);
359+
return query.aggregate(pipeline);
360+
}).then((results) => {
361+
expect(results.length).toEqual(1);
362+
expect(results[0].objectId).toEqual(obj1.id);
363+
done();
364+
});
365+
});
366+
367+
it('match pointer query', (done) => {
368+
const pointer1 = new TestObject();
369+
const pointer2 = new TestObject();
370+
const obj1 = new TestObject({ pointer: pointer1 });
371+
const obj2 = new TestObject({ pointer: pointer2 });
372+
const obj3 = new TestObject({ pointer: pointer1 });
373+
374+
Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]).then(() => {
375+
const pipeline = [
376+
{ match: { pointer: pointer1.id } }
377+
];
378+
const query = new Parse.Query(TestObject);
379+
return query.aggregate(pipeline);
380+
}).then((results) => {
381+
expect(results.length).toEqual(2);
382+
expect(results[0].pointer.objectId).toEqual(pointer1.id);
383+
expect(results[1].pointer.objectId).toEqual(pointer1.id);
384+
expect(results.some(result => result.objectId === obj1.id)).toEqual(true);
385+
expect(results.some(result => result.objectId === obj3.id)).toEqual(true);
386+
done();
387+
});
388+
});
389+
269390
it('project query', (done) => {
270391
const options = Object.assign({}, masterKeyOptions, {
271392
body: {

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,20 @@ export class MongoStorageAdapter implements StorageAdapter {
524524
stage.$group._id = `$_p_${field}`;
525525
}
526526
}
527+
if (stage.$match) {
528+
for (const field in stage.$match) {
529+
if (schema.fields[field] && schema.fields[field].type === 'Pointer') {
530+
const transformMatch = { [`_p_${field}`] : `${className}$${stage.$match[field]}` };
531+
stage.$match = transformMatch;
532+
}
533+
if (field === 'objectId') {
534+
const transformMatch = Object.assign({}, stage.$match);
535+
transformMatch._id = stage.$match[field];
536+
delete transformMatch.objectId;
537+
stage.$match = transformMatch;
538+
}
539+
}
540+
}
527541
return stage;
528542
});
529543
readPreference = this._parseReadPreference(readPreference);

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
15071507
aggregate(className: string, schema: any, pipeline: any) {
15081508
debug('aggregate', className, pipeline);
15091509
const values = [className];
1510+
let index = 2;
15101511
let columns: string[] = [];
15111512
let countField = null;
15121513
let wherePattern = '';
@@ -1523,26 +1524,38 @@ export class PostgresStorageAdapter implements StorageAdapter {
15231524
continue;
15241525
}
15251526
if (field === '_id') {
1526-
columns.push(`${transformAggregateField(value)} AS "objectId"`);
1527-
groupPattern = `GROUP BY ${transformAggregateField(value)}`;
1527+
columns.push(`$${index}:name AS "objectId"`);
1528+
groupPattern = `GROUP BY $${index}:name`;
1529+
values.push(transformAggregateField(value));
1530+
index += 1;
15281531
continue;
15291532
}
15301533
if (value.$sum) {
15311534
if (typeof value.$sum === 'string') {
1532-
columns.push(`SUM(${transformAggregateField(value.$sum)}) AS "${field}"`);
1535+
columns.push(`SUM($${index}:name) AS $${index + 1}:name`);
1536+
values.push(transformAggregateField(value.$sum), field);
1537+
index += 2;
15331538
} else {
15341539
countField = field;
1535-
columns.push(`COUNT(*) AS "${field}"`);
1540+
columns.push(`COUNT(*) AS $${index}:name`);
1541+
values.push(field);
1542+
index += 1;
15361543
}
15371544
}
15381545
if (value.$max) {
1539-
columns.push(`MAX(${transformAggregateField(value.$max)}) AS "${field}"`);
1546+
columns.push(`MAX($${index}:name) AS $${index + 1}:name`);
1547+
values.push(transformAggregateField(value.$max), field);
1548+
index += 2;
15401549
}
15411550
if (value.$min) {
1542-
columns.push(`MIN(${transformAggregateField(value.$min)}) AS "${field}"`);
1551+
columns.push(`MIN($${index}:name) AS $${index + 1}:name`);
1552+
values.push(transformAggregateField(value.$min), field);
1553+
index += 2;
15431554
}
15441555
if (value.$avg) {
1545-
columns.push(`AVG(${transformAggregateField(value.$avg)}) AS "${field}"`);
1556+
columns.push(`AVG($${index}:name) AS $${index + 1}:name`);
1557+
values.push(transformAggregateField(value.$avg), field);
1558+
index += 2;
15461559
}
15471560
}
15481561
} else {
@@ -1555,38 +1568,68 @@ export class PostgresStorageAdapter implements StorageAdapter {
15551568
for (const field in stage.$project) {
15561569
const value = stage.$project[field];
15571570
if ((value === 1 || value === true)) {
1558-
columns.push(field);
1571+
columns.push(`$${index}:name`);
1572+
values.push(field);
1573+
index += 1;
15591574
}
15601575
}
15611576
}
15621577
if (stage.$match) {
15631578
const patterns = [];
1579+
const orOrAnd = stage.$match.hasOwnProperty('$or') ? ' OR ' : ' AND ';
1580+
1581+
if (stage.$match.$or) {
1582+
const collapse = {};
1583+
stage.$match.$or.forEach((element) => {
1584+
for (const key in element) {
1585+
collapse[key] = element[key];
1586+
}
1587+
});
1588+
stage.$match = collapse;
1589+
}
15641590
for (const field in stage.$match) {
15651591
const value = stage.$match[field];
1566-
Object.keys(ParseToPosgresComparator).forEach(cmp => {
1592+
const matchPatterns = [];
1593+
Object.keys(ParseToPosgresComparator).forEach((cmp) => {
15671594
if (value[cmp]) {
15681595
const pgComparator = ParseToPosgresComparator[cmp];
1569-
patterns.push(`${field} ${pgComparator} ${value[cmp]}`);
1596+
matchPatterns.push(`$${index}:name ${pgComparator} $${index + 1}`);
1597+
values.push(field, toPostgresValue(value[cmp]));
1598+
index += 2;
15701599
}
15711600
});
1601+
if (matchPatterns.length > 0) {
1602+
patterns.push(`(${matchPatterns.join(' AND ')})`);
1603+
}
1604+
if (schema.fields[field] && schema.fields[field].type && matchPatterns.length === 0) {
1605+
patterns.push(`$${index}:name = $${index + 1}`);
1606+
values.push(field, value);
1607+
index += 2;
1608+
}
15721609
}
1573-
wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(' ')}` : '';
1610+
wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(` ${orOrAnd} `)}` : '';
15741611
}
15751612
if (stage.$limit) {
1576-
limitPattern = `LIMIT ${stage.$limit}`;
1613+
limitPattern = `LIMIT $${index}`;
1614+
values.push(stage.$limit);
1615+
index += 1;
15771616
}
15781617
if (stage.$skip) {
1579-
skipPattern = `OFFSET ${stage.$skip}`;
1618+
skipPattern = `OFFSET $${index}`;
1619+
values.push(stage.$skip);
1620+
index += 1;
15801621
}
15811622
if (stage.$sort) {
15821623
const sort = stage.$sort;
1583-
const sorting = Object.keys(sort).map((key) => {
1584-
if (sort[key] === 1) {
1585-
return `"${key}" ASC`;
1586-
}
1587-
return `"${key}" DESC`;
1624+
const keys = Object.keys(sort);
1625+
const sorting = keys.map((key) => {
1626+
const transformer = sort[key] === 1 ? 'ASC' : 'DESC';
1627+
const order = `$${index}:name ${transformer}`;
1628+
index += 1;
1629+
return order;
15881630
}).join();
1589-
sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : '';
1631+
values.push(...keys);
1632+
sortPattern = sort !== undefined && sorting.length > 0 ? `ORDER BY ${sorting}` : '';
15901633
}
15911634
}
15921635

0 commit comments

Comments
 (0)