PG: Add dates to group aggregate (#4549)
* PG: Add dates to group aggregate * returns dates as UTC
This commit is contained in:
@@ -96,6 +96,89 @@ describe('Parse.Query Aggregate testing', () => {
|
|||||||
}).catch(done.fail);
|
}).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) => {
|
it('group by pointer', (done) => {
|
||||||
const pointer1 = new TestObject();
|
const pointer1 = new TestObject();
|
||||||
const pointer2 = new TestObject();
|
const pointer2 = new TestObject();
|
||||||
|
|||||||
@@ -526,7 +526,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
|||||||
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) {
|
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) {
|
||||||
let isPointerField = false;
|
let isPointerField = false;
|
||||||
pipeline = pipeline.map((stage) => {
|
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);
|
const field = stage.$group._id.substring(1);
|
||||||
if (schema.fields[field] && schema.fields[field].type === 'Pointer') {
|
if (schema.fields[field] && schema.fields[field].type === 'Pointer') {
|
||||||
isPointerField = true;
|
isPointerField = true;
|
||||||
@@ -552,12 +552,21 @@ export class MongoStorageAdapter implements StorageAdapter {
|
|||||||
readPreference = this._parseReadPreference(readPreference);
|
readPreference = this._parseReadPreference(readPreference);
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
|
.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 => {
|
.then(results => {
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
if (result.hasOwnProperty('_id')) {
|
if (result.hasOwnProperty('_id')) {
|
||||||
if (isPointerField && result._id) {
|
if (isPointerField && result._id) {
|
||||||
result._id = result._id.split('$')[1];
|
result._id = result._id.split('$')[1];
|
||||||
}
|
}
|
||||||
|
if (result._id == null || _.isEmpty(result._id)) {
|
||||||
|
result._id = null;
|
||||||
|
}
|
||||||
result.objectId = result._id;
|
result.objectId = result._id;
|
||||||
delete result._id;
|
delete result._id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,21 @@ const ParseToPosgresComparator = {
|
|||||||
'$lte': '<='
|
'$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 => {
|
const toPostgresValue = value => {
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
if (value.__type === 'Date') {
|
if (value.__type === 'Date') {
|
||||||
@@ -179,6 +194,15 @@ const transformDotField = (fieldName) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const transformAggregateField = (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);
|
return fieldName.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1519,6 +1543,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|||||||
let index = 2;
|
let index = 2;
|
||||||
let columns: string[] = [];
|
let columns: string[] = [];
|
||||||
let countField = null;
|
let countField = null;
|
||||||
|
let groupValues = null;
|
||||||
let wherePattern = '';
|
let wherePattern = '';
|
||||||
let limitPattern = '';
|
let limitPattern = '';
|
||||||
let skipPattern = '';
|
let skipPattern = '';
|
||||||
@@ -1532,13 +1557,33 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (field === '_id') {
|
if (field === '_id' && (typeof value === 'string') && value !== '') {
|
||||||
columns.push(`$${index}:name AS "objectId"`);
|
columns.push(`$${index}:name AS "objectId"`);
|
||||||
groupPattern = `GROUP BY $${index}:name`;
|
groupPattern = `GROUP BY $${index}:name`;
|
||||||
values.push(transformAggregateField(value));
|
values.push(transformAggregateField(value));
|
||||||
index += 1;
|
index += 1;
|
||||||
continue;
|
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 (value.$sum) {
|
||||||
if (typeof value.$sum === 'string') {
|
if (typeof value.$sum === 'string') {
|
||||||
columns.push(`SUM($${index}:name) AS $${index + 1}:name`);
|
columns.push(`SUM($${index}:name) AS $${index + 1}:name`);
|
||||||
@@ -1646,13 +1691,20 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|||||||
debug(qs, values);
|
debug(qs, values);
|
||||||
return this._client.map(qs, values, a => this.postgresObjectToParseObject(className, a, schema))
|
return this._client.map(qs, values, a => this.postgresObjectToParseObject(className, a, schema))
|
||||||
.then(results => {
|
.then(results => {
|
||||||
if (countField) {
|
|
||||||
results[0][countField] = parseInt(results[0][countField], 10);
|
|
||||||
}
|
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
if (!result.hasOwnProperty('objectId')) {
|
if (!result.hasOwnProperty('objectId')) {
|
||||||
result.objectId = null;
|
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;
|
return results;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user