From c4fb2e45d88db5b1dbdca7ffcb08233f2007605e Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 20 Sep 2017 22:12:49 -0500 Subject: [PATCH 1/6] Support for Distinct Query --- spec/ParseQuery.spec.js | 24 +++++++++++++++++++ src/Adapters/Storage/Mongo/MongoCollection.js | 4 ++++ .../Storage/Mongo/MongoStorageAdapter.js | 7 ++++++ .../Postgres/PostgresStorageAdapter.js | 17 +++++++++++++ src/Controllers/DatabaseController.js | 7 ++++++ src/RestQuery.js | 1 + src/Routers/ClassesRouter.js | 5 +++- 7 files changed, 64 insertions(+), 1 deletion(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index da53ca6292..6ccc9384a5 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -5,6 +5,7 @@ 'use strict'; const Parse = require('parse/node'); +const rp = require('request-promise'); describe('Parse.Query testing', () => { it("basic query", function(done) { @@ -3007,4 +3008,27 @@ describe('Parse.Query testing', () => { done(); }, done.fail); }); + + it('distinct support', function(done) { + const score1 = new TestObject({score: 10}); + const score2 = new TestObject({score: 10}); + const score3 = new TestObject({score: 10}); + const score4 = new TestObject({score: 20}); + Parse.Object.saveAll([score1, score2, score3, score4]).then(() => { + const distinct = 'score'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(2); + expect(response.results.indexOf(10) > -1).toBe(true); + expect(response.results.indexOf(20) > -1).toBe(true); + done(); + }).catch(done.fail); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index ad1b458d25..65b3745e03 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -54,6 +54,10 @@ export default class MongoCollection { return findOperation.toArray(); } + distinct(field, query) { + return this._mongoCollection.distinct(field, query); + } + count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) { const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS, readPreference }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 3142712a3a..1aa2c57159 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -405,6 +405,13 @@ export class MongoStorageAdapter { })); } + // Finds unique values. + distinct(className, schema, query, fieldName) { + schema = convertParseSchemaToMongoSchema(schema); + return this._adaptiveCollection(className) + .then(collection => collection.distinct(fieldName, transformWhere(className, query, schema))); + } + _parseReadPreference(readPreference) { if (readPreference) { switch (readPreference) { diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index f2aec54788..e1c2ecc837 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1366,6 +1366,23 @@ export class PostgresStorageAdapter { }); } + // Finds unique values. + distinct(className, schema, query, fieldName) { + debug('count', className, query); + const values = [fieldName, className]; + const where = buildWhereClause({ schema, query, index: 3 }); + values.push(...where.values); + + const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; + const qs = `SELECT DISTINCT $1:name FROM $2:name ${wherePattern}`; + return this._client.any(qs, values).catch((err) => { + if (err.code === PostgresRelationDoesNotExistError) { + return []; + } + throw err; + }).then((results) => results.map(object => object[fieldName])); + } + performInitialization({ VolatileClassesSchemas }) { debug('performInitialization'); const promises = VolatileClassesSchemas.map((schema) => { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c1cdfdadca..9acce14ff3 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -782,6 +782,7 @@ DatabaseController.prototype.find = function(className, query, { limit, acl, sort = {}, + distinct, count, keys, op, @@ -853,6 +854,12 @@ DatabaseController.prototype.find = function(className, query, { } else { return this.adapter.count(className, schema, query, readPreference); } + } else if (distinct) { + if (!classExists) { + return []; + } else { + return this.adapter.distinct(className, schema, query, distinct); + } } else { if (!classExists) { return []; diff --git a/src/RestQuery.js b/src/RestQuery.js index 832149b145..5be07e1f99 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -86,6 +86,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl case 'count': this.doCount = true; break; + case 'distinct': case 'skip': case 'limit': case 'readPreference': diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 5793445a60..781edb98d1 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -103,7 +103,7 @@ export class ClassesRouter extends PromiseRouter { static optionsFromBody(body) { const allowConstraints = ['skip', 'limit', 'order', 'count', 'keys', - 'include', 'redirectClassNameForKey', 'where']; + 'include', 'redirectClassNameForKey', 'where', 'distinct']; for (const key of Object.keys(body)) { if (allowConstraints.indexOf(key) === -1) { @@ -131,6 +131,9 @@ export class ClassesRouter extends PromiseRouter { if (body.include) { options.include = String(body.include); } + if (body.distinct) { + options.distinct = String(body.distinct); + } return options; } From a690f74af48b4b4bc4037d87a2e2de7deea88111 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 20 Sep 2017 23:00:05 -0500 Subject: [PATCH 2/6] nested support for distinct --- spec/ParseQuery.spec.js | 25 +++++++++++++++++++ .../Postgres/PostgresStorageAdapter.js | 25 +++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 6ccc9384a5..ebc5133778 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3031,4 +3031,29 @@ describe('Parse.Query testing', () => { done(); }).catch(done.fail); }); + + it('nested distinct', (done) => { + const sender1 = { group: 'A' }; + const sender2 = { group: 'A' }; + const sender3 = { group: 'B' }; + const obj1 = new TestObject({ sender: sender1 }); + const obj2 = new TestObject({ sender: sender2 }); + const obj3 = new TestObject({ sender: sender3 }); + Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { + const distinct = 'sender.group'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(2); + expect(response.results.indexOf('A') > -1).toBe(true); + expect(response.results.indexOf('B') > -1).toBe(true); + done(); + }).catch(done.fail); + }); }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index e1c2ecc837..343d3bd8f7 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -732,6 +732,8 @@ export class PostgresStorageAdapter { deleteAllClasses() { const now = new Date().getTime(); debug('deleteAllClasses'); + /* eslint-disable */ + // return; return this._client.any('SELECT * FROM "_SCHEMA"') .then(results => { const joins = results.reduce((list, schema) => { @@ -1368,19 +1370,32 @@ export class PostgresStorageAdapter { // Finds unique values. distinct(className, schema, query, fieldName) { - debug('count', className, query); - const values = [fieldName, className]; - const where = buildWhereClause({ schema, query, index: 3 }); + debug('distinct', className, query); + let field = fieldName; + let column = fieldName; + if (fieldName.indexOf('.') >= 0) { + field = transformDotFieldToComponents(fieldName).join('->'); + column = fieldName.split('.')[0]; + } + const values = [field, column, className]; + const where = buildWhereClause({ schema, query, index: 4 }); values.push(...where.values); const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; - const qs = `SELECT DISTINCT $1:name FROM $2:name ${wherePattern}`; + const qs = `SELECT DISTINCT ON ($1:raw) $2:raw FROM $3:name ${wherePattern}`; + debug(qs, values); return this._client.any(qs, values).catch((err) => { if (err.code === PostgresRelationDoesNotExistError) { return []; } throw err; - }).then((results) => results.map(object => object[fieldName])); + }).then((results) => { + if (fieldName.indexOf('.') === -1) { + return results.map(object => object[field]); + } + const child = fieldName.split('.')[1]; + return results.map(object => object[column][child]); + }); } performInitialization({ VolatileClassesSchemas }) { From 2a1ce9678a6479ce88799c58a266e2fc23f872ce Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 20 Sep 2017 23:03:26 -0500 Subject: [PATCH 3/6] remove unnessary comment --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 343d3bd8f7..0e2c907b21 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -732,8 +732,6 @@ export class PostgresStorageAdapter { deleteAllClasses() { const now = new Date().getTime(); debug('deleteAllClasses'); - /* eslint-disable */ - // return; return this._client.any('SELECT * FROM "_SCHEMA"') .then(results => { const joins = results.reduce((list, schema) => { From 28f253879514dbe4f1d10e7f8aed2e3f260299e3 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 20 Sep 2017 23:38:25 -0500 Subject: [PATCH 4/6] class doesn't exist test --- spec/ParseQuery.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index ebc5133778..0493fbe3ad 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3056,4 +3056,19 @@ describe('Parse.Query testing', () => { done(); }).catch(done.fail); }); + + it('distinct empty when class does not exist', (done) => { + const distinct = 'unknown'; + rp.post({ + url: Parse.serverURL + "/classes/UnknownDistinct", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }).then((response) => { + expect(response.results.length).toBe(0); + done(); + }).catch(done.fail); + }); }); From a2bfab217caef25a51e2d1274c5e8f367b844ede Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 21 Sep 2017 01:45:31 -0500 Subject: [PATCH 5/6] distinct column doesn't exist test --- spec/ParseQuery.spec.js | 20 +++++++++++++++++- .../Postgres/PostgresStorageAdapter.js | 21 ++++++++----------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 0493fbe3ad..814e70efc0 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3057,7 +3057,7 @@ describe('Parse.Query testing', () => { }).catch(done.fail); }); - it('distinct empty when class does not exist', (done) => { + it('distinct class does not exist return empty', (done) => { const distinct = 'unknown'; rp.post({ url: Parse.serverURL + "/classes/UnknownDistinct", @@ -3071,4 +3071,22 @@ describe('Parse.Query testing', () => { done(); }).catch(done.fail); }); + + it('distinct field does not exist return empty', function(done) { + const score = new TestObject({score: 10}); + score.save().then(() => { + const distinct = 'unknown'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(0); + done(); + }).catch(done.fail); + }); }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 0e2c907b21..a5416f0d1c 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1382,18 +1382,15 @@ export class PostgresStorageAdapter { const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; const qs = `SELECT DISTINCT ON ($1:raw) $2:raw FROM $3:name ${wherePattern}`; debug(qs, values); - return this._client.any(qs, values).catch((err) => { - if (err.code === PostgresRelationDoesNotExistError) { - return []; - } - throw err; - }).then((results) => { - if (fieldName.indexOf('.') === -1) { - return results.map(object => object[field]); - } - const child = fieldName.split('.')[1]; - return results.map(object => object[column][child]); - }); + return this._client.any(qs, values) + .catch(() => []) + .then((results) => { + if (fieldName.indexOf('.') === -1) { + return results.map(object => object[field]); + } + const child = fieldName.split('.')[1]; + return results.map(object => object[column][child]); + }); } performInitialization({ VolatileClassesSchemas }) { From 748477adc552caa7d992657b2eb1f06d9b832159 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 21 Sep 2017 20:03:19 -0500 Subject: [PATCH 6/6] distinct array test --- spec/ParseQuery.spec.js | 28 +++++++++++++++++-- .../Postgres/PostgresStorageAdapter.js | 8 +++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 814e70efc0..7692d00fd8 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -3009,7 +3009,7 @@ describe('Parse.Query testing', () => { }, done.fail); }); - it('distinct support', function(done) { + it('distinct query', function(done) { const score1 = new TestObject({score: 10}); const score2 = new TestObject({score: 10}); const score3 = new TestObject({score: 10}); @@ -3032,7 +3032,7 @@ describe('Parse.Query testing', () => { }).catch(done.fail); }); - it('nested distinct', (done) => { + it('distinct nested', (done) => { const sender1 = { group: 'A' }; const sender2 = { group: 'A' }; const sender3 = { group: 'B' }; @@ -3089,4 +3089,28 @@ describe('Parse.Query testing', () => { done(); }).catch(done.fail); }); + + it('distinct array', function(done) { + const size1 = new TestObject({size: ['S', 'M']}); + const size2 = new TestObject({size: ['M', 'L']}); + const size3 = new TestObject({size: ['S']}); + const size4 = new TestObject({size: ['S']}); + Parse.Object.saveAll([size1, size2, size3, size4]).then(() => { + const distinct = 'size'; + return rp.post({ + url: Parse.serverURL + "/classes/TestObject", + json: { distinct, "_method": "GET" }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }).then((response) => { + expect(response.results.length).toBe(3); + expect(response.results.indexOf('S') > -1).toBe(true); + expect(response.results.indexOf('M') > -1).toBe(true); + expect(response.results.indexOf('L') > -1).toBe(true); + done(); + }).catch(done.fail); + }); }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index a5416f0d1c..a58309e26a 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1375,12 +1375,18 @@ export class PostgresStorageAdapter { field = transformDotFieldToComponents(fieldName).join('->'); column = fieldName.split('.')[0]; } + const isArrayField = schema.fields + && schema.fields[fieldName] + && schema.fields[fieldName].type === 'Array'; const values = [field, column, className]; const where = buildWhereClause({ schema, query, index: 4 }); values.push(...where.values); const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; - const qs = `SELECT DISTINCT ON ($1:raw) $2:raw FROM $3:name ${wherePattern}`; + let qs = `SELECT DISTINCT ON ($1:raw) $2:raw FROM $3:name ${wherePattern}`; + if (isArrayField) { + qs = `SELECT distinct jsonb_array_elements($1:raw) as $2:raw FROM $3:name ${wherePattern}`; + } debug(qs, values); return this._client.any(qs, values) .catch(() => [])