diff --git a/package-lock.json b/package-lock.json index 73f68403a1..c333799734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2362,14 +2362,14 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz", - "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.0.tgz", + "integrity": "sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g==", "dev": true, "requires": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", + "js-yaml": "^3.13.0", "lodash.get": "^4.4.2", "parse-json": "^4.0.0" } @@ -2907,14 +2907,14 @@ } }, "es5-ext": { - "version": "0.10.48", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.48.tgz", - "integrity": "sha512-CdRvPlX/24Mj5L4NVxTs4804sxiS2CjVprgCmrgoDkdmjdY4D+ySHa7K3jJf8R40dFg0tIm3z/dk326LrnuSGw==", + "version": "0.10.49", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.49.tgz", + "integrity": "sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==", "dev": true, "requires": { "es6-iterator": "~2.0.3", "es6-symbol": "~3.1.1", - "next-tick": "1" + "next-tick": "^1.0.0" } }, "es6-iterator": { @@ -3703,6 +3703,12 @@ "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=", "dev": true }, + "fn-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", + "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=", + "dev": true + }, "follow-redirects": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", @@ -4603,6 +4609,32 @@ "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "has-ansi": { @@ -5416,9 +5448,9 @@ "dev": true }, "js-yaml": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz", - "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", + "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -5617,6 +5649,15 @@ "graceful-fs": "^4.1.9" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -9122,9 +9163,9 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simple-git": { - "version": "1.107.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-1.107.0.tgz", - "integrity": "sha512-t4OK1JRlp4ayKRfcW6owrWcRVLyHRUlhGd0uN6ZZTqfDq8a5XpcUdOKiGRNobHEuMtNqzp0vcJNvhYWwh5PsQA==", + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-1.110.0.tgz", + "integrity": "sha512-UYY0rQkknk0P5eb+KW+03F4TevZ9ou0H+LoGaj7iiVgpnZH4wdj/HTViy/1tNNkmIPcmtxuBqXWiYt2YwlRKOQ==", "dev": true, "requires": { "debug": "^4.0.1" @@ -9569,6 +9610,12 @@ "integrity": "sha512-TyOuWLwkmtPL49LHCX1caIwHjRzcVd62+GF6h8W/jHOeZUFHpnd2XJDVuUlaTaLPH1nuu2M69mfHr5XbQJnf/g==", "dev": true }, + "synchronous-promise": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.6.tgz", + "integrity": "sha512-TyOuWLwkmtPL49LHCX1caIwHjRzcVd62+GF6h8W/jHOeZUFHpnd2XJDVuUlaTaLPH1nuu2M69mfHr5XbQJnf/g==", + "dev": true + }, "table": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz", diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 10686fdd37..85b84ffebf 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -145,7 +145,7 @@ describe('AudiencesRouter', () => { }); }); - it('query installations with count = 1', done => { + it_exclude_dbs(['postgres'])('query installations with count = 1', done => { const config = Config.get('test'); const androidAudienceRequest = { name: 'Android Users', @@ -189,7 +189,7 @@ describe('AudiencesRouter', () => { }); }); - it('query installations with limit = 0 and count = 1', done => { + it_exclude_dbs(['postgres'])('query installations with limit = 0 and count = 1', done => { const config = Config.get('test'); const androidAudienceRequest = { name: 'Android Users', diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 18103762e6..91935e645a 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -160,7 +160,7 @@ describe('InstallationsRouter', () => { }); }); - it('query installations with count = 1', done => { + it_exclude_dbs(['postgres'])('query installations with count = 1', done => { const config = Config.get('test'); const androidDeviceRequest = { installationId: '12345678-abcd-abcd-abcd-123456789abc', @@ -209,7 +209,7 @@ describe('InstallationsRouter', () => { }); }); - it('query installations with limit = 0 and count = 1', done => { + it_only_db('postgres')('query installations with count = 1', async () => { const config = Config.get('test'); const androidDeviceRequest = { installationId: '12345678-abcd-abcd-abcd-123456789abc', @@ -224,40 +224,90 @@ describe('InstallationsRouter', () => { auth: auth.master(config), body: {}, query: { - limit: 0, count: 1, }, info: {}, }; const router = new InstallationsRouter(); - rest - .create( - config, - auth.nobody(config), - '_Installation', - androidDeviceRequest - ) - .then(() => { - return rest.create( + await rest.create( + config, + auth.nobody(config), + '_Installation', + androidDeviceRequest + ); + await rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ); + let res = await router.handleFind(request); + let response = res.response; + expect(response.results.length).toEqual(2); + expect(response.count).toEqual(0); // estimate count is zero + + const pgAdapter = config.database.adapter; + await pgAdapter.updateEstimatedCount('_Installation'); + + res = await router.handleFind(request); + response = res.response; + expect(response.results.length).toEqual(2); + expect(response.count).toEqual(2); + }); + + it_exclude_dbs(['postgres'])( + 'query installations with limit = 0 and count = 1', + done => { + const config = Config.get('test'); + const androidDeviceRequest = { + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', + }; + const iosDeviceRequest = { + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', + }; + const request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + limit: 0, + count: 1, + }, + info: {}, + }; + + const router = new InstallationsRouter(); + rest + .create( config, auth.nobody(config), '_Installation', - iosDeviceRequest - ); - }) - .then(() => { - return router.handleFind(request); - }) - .then(res => { - const response = res.response; - expect(response.results.length).toEqual(0); - expect(response.count).toEqual(2); - done(); - }) - .catch(err => { - fail(JSON.stringify(err)); - done(); - }); - }); + androidDeviceRequest + ) + .then(() => { + return rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ); + }) + .then(() => { + return router.handleFind(request); + }) + .then(res => { + const response = res.response; + expect(response.results.length).toEqual(0); + expect(response.count).toEqual(2); + done(); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); + }); + } + ); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index b0e3351ee9..0e4c3fe810 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -152,10 +152,7 @@ export class MongoStorageAdapter implements StorageAdapter { // encoded const encodedUri = formatUrl(parseUrl(this._uri)); - this.connectionPromise = MongoClient.connect( - encodedUri, - this._mongoOptions - ) + this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions) .then(client => { // Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client // Fortunately, we can get back the options and use them to select the proper DB. @@ -385,8 +382,8 @@ export class MongoStorageAdapter implements StorageAdapter { deleteAllClasses(fast: boolean) { return storageAdapterAllCollections(this).then(collections => Promise.all( - collections.map( - collection => (fast ? collection.deleteMany({}) : collection.drop()) + collections.map(collection => + fast ? collection.deleteMany({}) : collection.drop() ) ) ); @@ -952,6 +949,8 @@ export class MongoStorageAdapter implements StorageAdapter { readPreference = ReadPreference.NEAREST; break; case undefined: + case null: + case '': break; default: throw new Parse.Error( diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index a424b5f2f2..70df7d8119 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1962,17 +1962,37 @@ export class PostgresStorageAdapter implements StorageAdapter { } // Executes a count. - count(className: string, schema: SchemaType, query: QueryType) { - debug('count', className, query); + count( + className: string, + schema: SchemaType, + query: QueryType, + readPreference?: string, + estimate?: boolean = true + ) { + debug('count', className, query, readPreference, estimate); const values = [className]; const where = buildWhereClause({ schema, query, index: 2 }); values.push(...where.values); const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; - const qs = `SELECT count(*) FROM $1:name ${wherePattern}`; + let qs = ''; + + if (where.pattern.length > 0 || !estimate) { + qs = `SELECT count(*) FROM $1:name ${wherePattern}`; + } else { + qs = + 'SELECT reltuples AS approximate_row_count FROM pg_class WHERE relname = $1'; + } + return this._client - .one(qs, values, a => +a.count) + .one(qs, values, a => { + if (a.approximate_row_count != null) { + return +a.approximate_row_count; + } else { + return +a.count; + } + }) .catch(error => { if (error.code !== PostgresRelationDoesNotExistError) { throw error; @@ -2327,6 +2347,11 @@ export class PostgresStorageAdapter implements StorageAdapter { updateSchemaWithIndexes(): Promise { return Promise.resolve(); } + + // Used for testing purposes + updateEstimatedCount(className: string) { + return this._client.none('ANALYZE $1:name', [className]); + } } function convertPolygonToSQL(polygon) { diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index d901452dcd..31afe569c9 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -86,7 +86,8 @@ export interface StorageAdapter { className: string, schema: SchemaType, query: QueryType, - readPreference: ?string + readPreference?: string, + estimate?: boolean ): Promise; distinct( className: string, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 9308cb42b9..cbbce0a064 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1324,7 +1324,9 @@ class DatabaseController { }) .then((schema: any) => { return this.collectionExists(className) - .then(() => this.adapter.count(className, { fields: {} })) + .then(() => + this.adapter.count(className, { fields: {} }, null, '', false) + ) .then(count => { if (count > 0) { throw new Parse.Error(