From 143b0f01cf177251e51e263e5628bb5dc211c4d6 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 16 Feb 2018 12:41:02 -0600 Subject: [PATCH] PG: Add dates to group aggregate (#4549) * PG: Add dates to group aggregate * returns dates as UTC --- spec/ParseQuery.Aggregate.spec.js | 83 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 11 ++- .../Postgres/PostgresStorageAdapter.js | 60 +++++++++++++- 3 files changed, 149 insertions(+), 5 deletions(-) diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 7e4bc6ff..dade21ca 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -96,6 +96,89 @@ describe('Parse.Query Aggregate testing', () => { }).catch(done.fail); }); + it('group by empty object', (done) => { + const obj = new TestObject(); + const pipeline = [{ + group: { objectId: {} } + }]; + obj.save().then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + expect(results[0].objectId).toEqual(null); + done(); + }); + }); + + it('group by empty string', (done) => { + const obj = new TestObject(); + const pipeline = [{ + group: { objectId: '' } + }]; + obj.save().then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + expect(results[0].objectId).toEqual(null); + done(); + }); + }); + + it('group by empty array', (done) => { + const obj = new TestObject(); + const pipeline = [{ + group: { objectId: [] } + }]; + obj.save().then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + expect(results[0].objectId).toEqual(null); + done(); + }); + }); + + it('group by date object', (done) => { + const obj1 = new TestObject(); + const obj2 = new TestObject(); + const obj3 = new TestObject(); + const pipeline = [{ + group: { + objectId: { day: { $dayOfMonth: "$_updated_at" }, month: { $month: "$_created_at" }, year: { $year: "$_created_at" } }, + count: { $sum: 1 } + } + }]; + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then((results) => { + const createdAt = new Date(obj1.createdAt); + expect(results[0].objectId.day).toEqual(createdAt.getUTCDate()); + expect(results[0].objectId.month).toEqual(createdAt.getMonth() + 1); + expect(results[0].objectId.year).toEqual(createdAt.getUTCFullYear()); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('cannot group by date field (excluding createdAt and updatedAt)', (done) => { + const obj1 = new TestObject({ dateField: new Date(1990, 11, 1) }); + const obj2 = new TestObject({ dateField: new Date(1990, 5, 1) }); + const obj3 = new TestObject({ dateField: new Date(1990, 11, 1) }); + const pipeline = [{ + group: { + objectId: { day: { $dayOfMonth: "$dateField" }, month: { $month: "$dateField" }, year: { $year: "$dateField" } }, + count: { $sum: 1 } + } + }]; + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }).then(done.fail).catch((error) => { + expect(error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); + }); + }); + it('group by pointer', (done) => { const pointer1 = new TestObject(); const pointer2 = new TestObject(); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 11218f97..a7b33105 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -526,7 +526,7 @@ export class MongoStorageAdapter implements StorageAdapter { aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) { let isPointerField = false; pipeline = pipeline.map((stage) => { - if (stage.$group && stage.$group._id) { + if (stage.$group && stage.$group._id && (typeof stage.$group._id === 'string')) { const field = stage.$group._id.substring(1); if (schema.fields[field] && schema.fields[field].type === 'Pointer') { isPointerField = true; @@ -552,12 +552,21 @@ export class MongoStorageAdapter implements StorageAdapter { readPreference = this._parseReadPreference(readPreference); return this._adaptiveCollection(className) .then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS })) + .catch(error => { + if (error.code === 16006) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, error.message); + } + throw error; + }) .then(results => { results.forEach(result => { if (result.hasOwnProperty('_id')) { if (isPointerField && result._id) { result._id = result._id.split('$')[1]; } + if (result._id == null || _.isEmpty(result._id)) { + result._id = null; + } result.objectId = result._id; delete result._id; } diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 7cfbced8..ef005c59 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -55,6 +55,21 @@ const ParseToPosgresComparator = { '$lte': '<=' } +const mongoAggregateToPostgres = { + $dayOfMonth: 'DAY', + $dayOfWeek: 'DOW', + $dayOfYear: 'DOY', + $isoDayOfWeek: 'ISODOW', + $isoWeekYear:'ISOYEAR', + $hour: 'HOUR', + $minute: 'MINUTE', + $second: 'SECOND', + $millisecond: 'MILLISECONDS', + $month: 'MONTH', + $week: 'WEEK', + $year: 'YEAR', +}; + const toPostgresValue = value => { if (typeof value === 'object') { if (value.__type === 'Date') { @@ -179,6 +194,15 @@ const transformDotField = (fieldName) => { } const transformAggregateField = (fieldName) => { + if (typeof fieldName !== 'string') { + return fieldName; + } + if (fieldName === '$_created_at') { + return 'createdAt'; + } + if (fieldName === '$_updated_at') { + return 'updatedAt'; + } return fieldName.substr(1); } @@ -1519,6 +1543,7 @@ export class PostgresStorageAdapter implements StorageAdapter { let index = 2; let columns: string[] = []; let countField = null; + let groupValues = null; let wherePattern = ''; let limitPattern = ''; let skipPattern = ''; @@ -1532,13 +1557,33 @@ export class PostgresStorageAdapter implements StorageAdapter { if (value === null || value === undefined) { continue; } - if (field === '_id') { + if (field === '_id' && (typeof value === 'string') && value !== '') { columns.push(`$${index}:name AS "objectId"`); groupPattern = `GROUP BY $${index}:name`; values.push(transformAggregateField(value)); index += 1; continue; } + if (field === '_id' && (typeof value === 'object') && Object.keys(value).length !== 0) { + groupValues = value; + const groupByFields = []; + for (const alias in value) { + const operation = Object.keys(value[alias])[0]; + const source = transformAggregateField(value[alias][operation]); + if (mongoAggregateToPostgres[operation]) { + if (!groupByFields.includes(`"${source}"`)) { + groupByFields.push(`"${source}"`); + } + columns.push(`EXTRACT(${mongoAggregateToPostgres[operation]} FROM $${index}:name AT TIME ZONE 'UTC') AS $${index + 1}:name`); + values.push(source, alias); + index += 2; + } + } + groupPattern = `GROUP BY $${index}:raw`; + values.push(groupByFields.join()); + index += 1; + continue; + } if (value.$sum) { if (typeof value.$sum === 'string') { columns.push(`SUM($${index}:name) AS $${index + 1}:name`); @@ -1646,13 +1691,20 @@ export class PostgresStorageAdapter implements StorageAdapter { debug(qs, values); return this._client.map(qs, values, a => this.postgresObjectToParseObject(className, a, schema)) .then(results => { - if (countField) { - results[0][countField] = parseInt(results[0][countField], 10); - } results.forEach(result => { if (!result.hasOwnProperty('objectId')) { result.objectId = null; } + if (groupValues) { + result.objectId = {}; + for (const key in groupValues) { + result.objectId[key] = result[key]; + delete result[key]; + } + } + if (countField) { + result[countField] = parseInt(result[countField], 10); + } }); return results; });