Skip to content

Commit 09c0e88

Browse files
authored
PG: Add dates to group aggregate (parse-community#4549)
* PG: Add dates to group aggregate * returns dates as UTC
1 parent f475801 commit 09c0e88

File tree

3 files changed

+149
-5
lines changed

3 files changed

+149
-5
lines changed

spec/ParseQuery.Aggregate.spec.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,89 @@ describe('Parse.Query Aggregate testing', () => {
9696
}).catch(done.fail);
9797
});
9898

99+
it('group by empty object', (done) => {
100+
const obj = new TestObject();
101+
const pipeline = [{
102+
group: { objectId: {} }
103+
}];
104+
obj.save().then(() => {
105+
const query = new Parse.Query(TestObject);
106+
return query.aggregate(pipeline);
107+
}).then((results) => {
108+
expect(results[0].objectId).toEqual(null);
109+
done();
110+
});
111+
});
112+
113+
it('group by empty string', (done) => {
114+
const obj = new TestObject();
115+
const pipeline = [{
116+
group: { objectId: '' }
117+
}];
118+
obj.save().then(() => {
119+
const query = new Parse.Query(TestObject);
120+
return query.aggregate(pipeline);
121+
}).then((results) => {
122+
expect(results[0].objectId).toEqual(null);
123+
done();
124+
});
125+
});
126+
127+
it('group by empty array', (done) => {
128+
const obj = new TestObject();
129+
const pipeline = [{
130+
group: { objectId: [] }
131+
}];
132+
obj.save().then(() => {
133+
const query = new Parse.Query(TestObject);
134+
return query.aggregate(pipeline);
135+
}).then((results) => {
136+
expect(results[0].objectId).toEqual(null);
137+
done();
138+
});
139+
});
140+
141+
it('group by date object', (done) => {
142+
const obj1 = new TestObject();
143+
const obj2 = new TestObject();
144+
const obj3 = new TestObject();
145+
const pipeline = [{
146+
group: {
147+
objectId: { day: { $dayOfMonth: "$_updated_at" }, month: { $month: "$_created_at" }, year: { $year: "$_created_at" } },
148+
count: { $sum: 1 }
149+
}
150+
}];
151+
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
152+
const query = new Parse.Query(TestObject);
153+
return query.aggregate(pipeline);
154+
}).then((results) => {
155+
const createdAt = new Date(obj1.createdAt);
156+
expect(results[0].objectId.day).toEqual(createdAt.getUTCDate());
157+
expect(results[0].objectId.month).toEqual(createdAt.getMonth() + 1);
158+
expect(results[0].objectId.year).toEqual(createdAt.getUTCFullYear());
159+
done();
160+
});
161+
});
162+
163+
it_exclude_dbs(['postgres'])('cannot group by date field (excluding createdAt and updatedAt)', (done) => {
164+
const obj1 = new TestObject({ dateField: new Date(1990, 11, 1) });
165+
const obj2 = new TestObject({ dateField: new Date(1990, 5, 1) });
166+
const obj3 = new TestObject({ dateField: new Date(1990, 11, 1) });
167+
const pipeline = [{
168+
group: {
169+
objectId: { day: { $dayOfMonth: "$dateField" }, month: { $month: "$dateField" }, year: { $year: "$dateField" } },
170+
count: { $sum: 1 }
171+
}
172+
}];
173+
Parse.Object.saveAll([obj1, obj2, obj3]).then(() => {
174+
const query = new Parse.Query(TestObject);
175+
return query.aggregate(pipeline);
176+
}).then(done.fail).catch((error) => {
177+
expect(error.code).toEqual(Parse.Error.INVALID_QUERY);
178+
done();
179+
});
180+
});
181+
99182
it('group by pointer', (done) => {
100183
const pointer1 = new TestObject();
101184
const pointer2 = new TestObject();

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ export class MongoStorageAdapter implements StorageAdapter {
526526
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) {
527527
let isPointerField = false;
528528
pipeline = pipeline.map((stage) => {
529-
if (stage.$group && stage.$group._id) {
529+
if (stage.$group && stage.$group._id && (typeof stage.$group._id === 'string')) {
530530
const field = stage.$group._id.substring(1);
531531
if (schema.fields[field] && schema.fields[field].type === 'Pointer') {
532532
isPointerField = true;
@@ -552,12 +552,21 @@ export class MongoStorageAdapter implements StorageAdapter {
552552
readPreference = this._parseReadPreference(readPreference);
553553
return this._adaptiveCollection(className)
554554
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
555+
.catch(error => {
556+
if (error.code === 16006) {
557+
throw new Parse.Error(Parse.Error.INVALID_QUERY, error.message);
558+
}
559+
throw error;
560+
})
555561
.then(results => {
556562
results.forEach(result => {
557563
if (result.hasOwnProperty('_id')) {
558564
if (isPointerField && result._id) {
559565
result._id = result._id.split('$')[1];
560566
}
567+
if (result._id == null || _.isEmpty(result._id)) {
568+
result._id = null;
569+
}
561570
result.objectId = result._id;
562571
delete result._id;
563572
}

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ const ParseToPosgresComparator = {
5555
'$lte': '<='
5656
}
5757

58+
const mongoAggregateToPostgres = {
59+
$dayOfMonth: 'DAY',
60+
$dayOfWeek: 'DOW',
61+
$dayOfYear: 'DOY',
62+
$isoDayOfWeek: 'ISODOW',
63+
$isoWeekYear:'ISOYEAR',
64+
$hour: 'HOUR',
65+
$minute: 'MINUTE',
66+
$second: 'SECOND',
67+
$millisecond: 'MILLISECONDS',
68+
$month: 'MONTH',
69+
$week: 'WEEK',
70+
$year: 'YEAR',
71+
};
72+
5873
const toPostgresValue = value => {
5974
if (typeof value === 'object') {
6075
if (value.__type === 'Date') {
@@ -179,6 +194,15 @@ const transformDotField = (fieldName) => {
179194
}
180195

181196
const transformAggregateField = (fieldName) => {
197+
if (typeof fieldName !== 'string') {
198+
return fieldName;
199+
}
200+
if (fieldName === '$_created_at') {
201+
return 'createdAt';
202+
}
203+
if (fieldName === '$_updated_at') {
204+
return 'updatedAt';
205+
}
182206
return fieldName.substr(1);
183207
}
184208

@@ -1519,6 +1543,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
15191543
let index = 2;
15201544
let columns: string[] = [];
15211545
let countField = null;
1546+
let groupValues = null;
15221547
let wherePattern = '';
15231548
let limitPattern = '';
15241549
let skipPattern = '';
@@ -1532,13 +1557,33 @@ export class PostgresStorageAdapter implements StorageAdapter {
15321557
if (value === null || value === undefined) {
15331558
continue;
15341559
}
1535-
if (field === '_id') {
1560+
if (field === '_id' && (typeof value === 'string') && value !== '') {
15361561
columns.push(`$${index}:name AS "objectId"`);
15371562
groupPattern = `GROUP BY $${index}:name`;
15381563
values.push(transformAggregateField(value));
15391564
index += 1;
15401565
continue;
15411566
}
1567+
if (field === '_id' && (typeof value === 'object') && Object.keys(value).length !== 0) {
1568+
groupValues = value;
1569+
const groupByFields = [];
1570+
for (const alias in value) {
1571+
const operation = Object.keys(value[alias])[0];
1572+
const source = transformAggregateField(value[alias][operation]);
1573+
if (mongoAggregateToPostgres[operation]) {
1574+
if (!groupByFields.includes(`"${source}"`)) {
1575+
groupByFields.push(`"${source}"`);
1576+
}
1577+
columns.push(`EXTRACT(${mongoAggregateToPostgres[operation]} FROM $${index}:name AT TIME ZONE 'UTC') AS $${index + 1}:name`);
1578+
values.push(source, alias);
1579+
index += 2;
1580+
}
1581+
}
1582+
groupPattern = `GROUP BY $${index}:raw`;
1583+
values.push(groupByFields.join());
1584+
index += 1;
1585+
continue;
1586+
}
15421587
if (value.$sum) {
15431588
if (typeof value.$sum === 'string') {
15441589
columns.push(`SUM($${index}:name) AS $${index + 1}:name`);
@@ -1646,13 +1691,20 @@ export class PostgresStorageAdapter implements StorageAdapter {
16461691
debug(qs, values);
16471692
return this._client.map(qs, values, a => this.postgresObjectToParseObject(className, a, schema))
16481693
.then(results => {
1649-
if (countField) {
1650-
results[0][countField] = parseInt(results[0][countField], 10);
1651-
}
16521694
results.forEach(result => {
16531695
if (!result.hasOwnProperty('objectId')) {
16541696
result.objectId = null;
16551697
}
1698+
if (groupValues) {
1699+
result.objectId = {};
1700+
for (const key in groupValues) {
1701+
result.objectId[key] = result[key];
1702+
delete result[key];
1703+
}
1704+
}
1705+
if (countField) {
1706+
result[countField] = parseInt(result[countField], 10);
1707+
}
16561708
});
16571709
return results;
16581710
});

0 commit comments

Comments
 (0)