From ad9998a1b202ded1c13a4996dccf52e887fed254 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Thu, 5 Oct 2017 23:47:56 -0500 Subject: [PATCH 01/11] Add Indexes to Schema API --- spec/schemas.spec.js | 346 ++++++++++++++++++ .../Storage/Mongo/MongoSchemaCollection.js | 5 + .../Storage/Mongo/MongoStorageAdapter.js | 54 ++- .../Postgres/PostgresStorageAdapter.js | 15 + src/Controllers/SchemaController.js | 67 ++-- src/Routers/SchemasRouter.js | 4 +- 6 files changed, 466 insertions(+), 25 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index b590460620..30857791d4 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1756,4 +1756,350 @@ describe('schemas', () => { done(); }); }); + + it_exclude_dbs(['postgres'])('allows add indexes when you create a class', done => { + request.post({ + url: 'http://localhost:8378/1/schemas', + headers: masterKeyHeaders, + json: true, + body: { + className: "NewClass", + indexes: { + name1: { field1: 1}, + name2: { field2: 1}, + } + } + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1}, + name2: { field2: 1}, + }, + }); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('lets you add indexes', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field1: 1 } + } + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + } + })).toEqual(undefined); + request.get({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + } + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes.length).toEqual(2); + done(); + }); + }); + }); + }) + }); + + it_exclude_dbs(['postgres'])('lets you add multiple indexes', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + } + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + } + })).toEqual(undefined); + request.get({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + }, + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes.length).toEqual(4); + done(); + }); + }); + }); + }) + }); + + it_exclude_dbs(['postgres'])('lets you delete indexes', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field1: 1 } + } + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + } + })).toEqual(undefined); + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { __op: 'Delete' } + } + } + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes.length).toEqual(1); + done(); + }); + }); + }); + }) + }); + + it_exclude_dbs(['postgres'])('lets you delete multiple indexes', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + } + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + } + })).toEqual(undefined); + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { __op: 'Delete' }, + name2: { __op: 'Delete' }, + } + } + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name3: { field3: 1 }, + } + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes.length).toEqual(2); + done(); + }); + }); + }); + }) + }); + + it_exclude_dbs(['postgres'])('lets you add and delete indexes', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + } + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { field1: 1 }, + name2: { field2: 1 }, + name3: { field3: 1 }, + } + })).toEqual(undefined); + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { __op: 'Delete' }, + name2: { __op: 'Delete' }, + name4: { field4: 1 }, + } + } + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name3: { field3: 1 }, + name4: { field4: 1 }, + } + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes.length).toEqual(3); + done(); + }); + }); + }); + }) + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 051bac65cf..3ccc318c1b 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -63,13 +63,18 @@ const defaultCLPS = Object.freeze({ function mongoSchemaToParseSchema(mongoSchema) { let clps = defaultCLPS; + let indexes = {} if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) { clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions}; } + if (mongoSchema._metadata && mongoSchema._metadata.indexes) { + indexes = {...mongoSchema._metadata.indexes}; + } return { className: mongoSchema._id, fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema), classLevelPermissions: clps, + indexes: indexes, }; } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 3142712a3a..e1191a0cee 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -53,7 +53,7 @@ const convertParseSchemaToMongoSchema = ({...schema}) => { // Returns { code, error } if invalid, or { result }, an object // suitable for inserting into _SCHEMA collection, otherwise. -const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions) => { +const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions, indexes) => { const mongoObject = { _id: className, objectId: 'string', @@ -74,6 +74,15 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe } } + if (typeof indexes !== 'undefined') { + mongoObject._metadata = mongoObject._metadata || {}; + if (!indexes) { + delete mongoObject._metadata.indexes; + } else { + mongoObject._metadata.indexes = indexes; + } + } + return mongoObject; } @@ -164,9 +173,40 @@ export class MongoStorageAdapter { })); } + setIndexes(className, submittedIndexes, existingIndexes = {}) { + if (submittedIndexes === undefined) { + return Promise.resolve(); + } + const deletePromises = []; + const insertedIndexes = []; + Object.keys(submittedIndexes).forEach(indexName => { + if (submittedIndexes[indexName].__op === 'Delete') { + const promise = this.dropIndex(className, indexName); + deletePromises.push(promise); + delete existingIndexes[indexName]; + } else { + existingIndexes[indexName] = submittedIndexes[indexName]; + insertedIndexes.push({ + key: submittedIndexes[indexName], + name: indexName, + }); + } + }); + let insertPromise = Promise.resolve(); + if (insertedIndexes.length > 0) { + insertPromise = this.createIndexes(className, insertedIndexes); + } + return Promise.all(deletePromises) + .then(() => insertPromise) + .then(() => this._schemaCollection()) + .then(schemaCollection => schemaCollection.updateSchema(className, { + $set: { _metadata: { indexes: existingIndexes } } + })); + } + createClass(className, schema) { schema = convertParseSchemaToMongoSchema(schema); - const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions); + const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes); mongoObject._id = className; return this._schemaCollection() .then(schemaCollection => schemaCollection._collection.insertOne(mongoObject)) @@ -439,6 +479,11 @@ export class MongoStorageAdapter { .then(collection => collection._mongoCollection.createIndex(index)); } + createIndexes(className, indexes) { + return this._adaptiveCollection(className) + .then(collection => collection._mongoCollection.createIndexes(indexes)); + } + createIndexesIfNeeded(className, fieldName, type) { if (type && type.type === 'Polygon') { const index = { @@ -474,6 +519,11 @@ export class MongoStorageAdapter { return this._adaptiveCollection(className) .then(collection => collection._mongoCollection.indexes()); } + + dropIndex(className, index) { + return this._adaptiveCollection(className) + .then(collection => collection._mongoCollection.dropIndex(index)); + } } export default MongoStorageAdapter; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index f2aec54788..689856d755 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -98,10 +98,15 @@ const toParseSchema = (schema) => { if (schema.classLevelPermissions) { clps = {...emptyCLPS, ...schema.classLevelPermissions}; } + let indexes = {}; + if (schema.classLevelPermissions) { + indexes = {...schema.indexes}; + } return { className: schema.className, fields: schema.fields, classLevelPermissions: clps, + indexes, }; } @@ -597,6 +602,16 @@ export class PostgresStorageAdapter { }); } + setIndexes(className, submittedIndexes, existingIndexes = {}) { + if (submittedIndexes === undefined) { + return Promise.resolve(); + } + return this._ensureSchemaCollectionExists().then(() => { + const values = [className, 'schema', 'indexes', JSON.stringify(existingIndexes)] + return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values); + }); + } + createClass(className, schema) { return this._client.tx(t => { const q1 = this.createTable(className, schema, t); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 6c60575bce..1170a694bf 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -286,18 +286,28 @@ const convertAdapterSchemaToParseSchema = ({...schema}) => { schema.fields.password = { type: 'String' }; } + if (schema.indexes && Object.keys(schema.indexes).length === 0) { + delete schema.indexes; + } + return schema; } -const injectDefaultSchema = ({className, fields, classLevelPermissions}) => ({ - className, - fields: { - ...defaultColumns._Default, - ...(defaultColumns[className] || {}), - ...fields, - }, - classLevelPermissions, -}); +const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}) => { + const defaultSchema = { + className, + fields: { + ...defaultColumns._Default, + ...(defaultColumns[className] || {}), + ...fields, + }, + classLevelPermissions, + }; + if (indexes && Object.keys(indexes).length !== 0) { + defaultSchema.indexes = indexes; + } + return defaultSchema; +}; const _HooksSchema = {className: "_Hooks", fields: defaultColumns._Hooks}; const _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig } @@ -343,6 +353,7 @@ export default class SchemaController { _dbAdapter; data; perms; + indexes; constructor(databaseAdapter, schemaCache) { this._dbAdapter = databaseAdapter; @@ -351,6 +362,8 @@ export default class SchemaController { this.data = {}; // this.perms[className][operation] tells you the acl-style permissions this.perms = {}; + // this.indexes[className][operation] tells you the indexes + this.indexes = {}; } reloadData(options = {clearCache: false}) { @@ -369,9 +382,11 @@ export default class SchemaController { .then(allSchemas => { const data = {}; const perms = {}; + const indexes = {}; allSchemas.forEach(schema => { data[schema.className] = injectDefaultSchema(schema).fields; perms[schema.className] = schema.classLevelPermissions; + indexes[schema.className] = schema.indexes; }); // Inject the in-memory classes @@ -379,13 +394,16 @@ export default class SchemaController { const schema = injectDefaultSchema({ className }); data[className] = schema.fields; perms[className] = schema.classLevelPermissions; + indexes[className] = schema.indexes; }); this.data = data; this.perms = perms; + this.indexes = indexes; delete this.reloadDataPromise; }, (err) => { this.data = {}; this.perms = {}; + this.indexes = {}; delete this.reloadDataPromise; throw err; }); @@ -423,7 +441,8 @@ export default class SchemaController { return Promise.resolve({ className, fields: this.data[className], - classLevelPermissions: this.perms[className] + classLevelPermissions: this.perms[className], + indexes: this.indexes[className] }); } return this._cache.getOneSchema(className).then((cached) => { @@ -448,13 +467,13 @@ export default class SchemaController { // on success, and rejects with an error on fail. Ensure you // have authorization (master key, or client class creation // enabled) before calling this function. - addClassIfNotExists(className, fields = {}, classLevelPermissions) { + addClassIfNotExists(className, fields = {}, classLevelPermissions, indexes = {}) { var validationError = this.validateNewClass(className, fields, classLevelPermissions); if (validationError) { return Promise.reject(validationError); } - return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className })) + return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, indexes, className })) .then(convertAdapterSchemaToParseSchema) .then((res) => { return this._cache.clear().then(() => { @@ -470,7 +489,7 @@ export default class SchemaController { }); } - updateClass(className, submittedFields, classLevelPermissions, database) { + updateClass(className, submittedFields, classLevelPermissions, indexes, database) { return this.getOneSchema(className) .then(schema => { const existingFields = schema.fields; @@ -508,7 +527,6 @@ export default class SchemaController { if (deletedFields.length > 0) { deletePromise = this.deleteFields(deletedFields, className, database); } - return deletePromise // Delete Everything .then(() => this.reloadData({ clearCache: true })) // Reload our Schema, so we have all the new values .then(() => { @@ -519,12 +537,20 @@ export default class SchemaController { return Promise.all(promises); }) .then(() => this.setPermissions(className, classLevelPermissions, newSchema)) + .then(() => this._dbAdapter.setIndexes(className, indexes, schema.indexes, newSchema)) + .then(() => this.reloadData({ clearCache: true })) //TODO: Move this logic into the database adapter - .then(() => ({ - className: className, - fields: this.data[className], - classLevelPermissions: this.perms[className] - })); + .then(() => { + const reloadedSchema = { + className: className, + fields: this.data[className], + classLevelPermissions: this.perms[className], + }; + if (this.indexes[className] && Object.keys(this.indexes[className]).length !== 0) { + reloadedSchema.indexes = this.indexes[className]; + } + return reloadedSchema; + }); }) .catch(error => { if (error === undefined) { @@ -619,8 +645,7 @@ export default class SchemaController { return Promise.resolve(); } validateCLP(perms, newSchema); - return this._dbAdapter.setClassLevelPermissions(className, perms) - .then(() => this.reloadData({ clearCache: true })); + return this._dbAdapter.setClassLevelPermissions(className, perms); } // Returns a promise that resolves successfully to the new schema diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 1fac1e88fa..8a334acdaa 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -46,7 +46,7 @@ function createSchema(req) { } return req.config.database.loadSchema({ clearCache: true}) - .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions)) + .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions, req.body.indexes)) .then(schema => ({ response: schema })); } @@ -59,7 +59,7 @@ function modifySchema(req) { const className = req.params.className; return req.config.database.loadSchema({ clearCache: true}) - .then(schema => schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database)) + .then(schema => schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.body.indexes, req.config.database)) .then(result => ({response: result})); } From e544c91c5db9789b0e3a2cd7d51964c4b94ebfd0 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 6 Oct 2017 01:18:47 -0500 Subject: [PATCH 02/11] error handling --- spec/schemas.spec.js | 70 +++++++++++++++++-- .../Storage/Mongo/MongoStorageAdapter.js | 24 ++++--- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 30857791d4..35e16f3644 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1784,7 +1784,10 @@ describe('schemas', () => { name2: { field2: 1}, }, }); - done(); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes.length).toBe(3); + done(); + }); }); }); @@ -1860,7 +1863,7 @@ describe('schemas', () => { indexes: { name1: { field1: 1 }, name2: { field2: 1 }, - name3: { field3: 1 }, + name3: { field3: 1, field4: 1 }, } } }, (error, response, body) => { @@ -1876,7 +1879,7 @@ describe('schemas', () => { indexes: { name1: { field1: 1 }, name2: { field2: 1 }, - name3: { field3: 1 }, + name3: { field3: 1, field4: 1 }, } })).toEqual(undefined); request.get({ @@ -1896,7 +1899,7 @@ describe('schemas', () => { indexes: { name1: { field1: 1 }, name2: { field2: 1 }, - name3: { field3: 1 }, + name3: { field3: 1, field4: 1 }, }, }); config.database.adapter.getIndexes('NewClass').then((indexes) => { @@ -2102,4 +2105,63 @@ describe('schemas', () => { }); }) }); + + it_exclude_dbs(['postgres'])('cannot delete index that does not exist', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + unknownIndex: { __op: 'Delete' } + } + } + }, (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe('Index unknownIndex does not exist, cannot delete.'); + done(); + }); + }) + }); + + it_exclude_dbs(['postgres'])('cannot update index that exist', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field1: 1 } + } + } + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field2: 1 } + } + } + }, (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe('Index name1 exists, cannot update.'); + done(); + }); + }); + }) + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index e1191a0cee..238f2dcbf3 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -179,16 +179,23 @@ export class MongoStorageAdapter { } const deletePromises = []; const insertedIndexes = []; - Object.keys(submittedIndexes).forEach(indexName => { - if (submittedIndexes[indexName].__op === 'Delete') { - const promise = this.dropIndex(className, indexName); + Object.keys(submittedIndexes).forEach(name => { + const field = submittedIndexes[name]; + if (existingIndexes[name] && field.__op !== 'Delete') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`); + } + if (!existingIndexes[name] && field.__op === 'Delete') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`); + } + if (field.__op === 'Delete') { + const promise = this.dropIndex(className, name); deletePromises.push(promise); - delete existingIndexes[indexName]; + delete existingIndexes[name]; } else { - existingIndexes[indexName] = submittedIndexes[indexName]; + existingIndexes[name] = field; insertedIndexes.push({ - key: submittedIndexes[indexName], - name: indexName, + key: field, + name, }); } }); @@ -208,7 +215,8 @@ export class MongoStorageAdapter { schema = convertParseSchemaToMongoSchema(schema); const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes); mongoObject._id = className; - return this._schemaCollection() + return this.setIndexes(className, schema.indexes, {}, schema) + .then(() => this._schemaCollection()) .then(schemaCollection => schemaCollection._collection.insertOne(mongoObject)) .then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0])) .catch(error => { From 834139057d822bea8c09cbbc23885a594e07eaf1 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 6 Oct 2017 07:30:10 -0500 Subject: [PATCH 03/11] ci errors --- spec/MongoSchemaCollectionAdapter.spec.js | 8 +++++++- spec/Schema.spec.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/MongoSchemaCollectionAdapter.spec.js b/spec/MongoSchemaCollectionAdapter.spec.js index ba22c1fab3..f5f75442f6 100644 --- a/spec/MongoSchemaCollectionAdapter.spec.js +++ b/spec/MongoSchemaCollectionAdapter.spec.js @@ -21,6 +21,9 @@ describe('MongoSchemaCollection', () => { "create":{"*":true}, "delete":{"*":true}, "addField":{"*":true}, + }, + "indexes": { + "name1":{"deviceToken":1} } }, "installationId":"string", @@ -66,7 +69,10 @@ describe('MongoSchemaCollection', () => { update: { '*': true }, delete: { '*': true }, addField: { '*': true }, - } + }, + indexes: { + name1: {deviceToken: 1} + }, }); done(); }); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index dcccb970f3..b1fb701353 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -274,7 +274,7 @@ describe('SchemaController', () => { fooSixteen: {type: 'String'}, fooEighteen: {type: 'String'}, fooNineteen: {type: 'String'}, - }, levelPermissions, config.database)) + }, levelPermissions, {}, config.database)) .then(actualSchema => { const expectedSchema = { className: 'NewClass', From af01977a354e5d0d836c1a3638d50d9fad918192 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 6 Oct 2017 11:22:44 -0500 Subject: [PATCH 04/11] postgres support --- spec/schemas.spec.js | 227 ++++++++++++++---- .../Storage/Mongo/MongoStorageAdapter.js | 17 +- .../Postgres/PostgresStorageAdapter.js | 81 ++++++- 3 files changed, 264 insertions(+), 61 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 35e16f3644..f5dca7d7db 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -771,7 +771,7 @@ describe('schemas', () => { }); }); - it_exclude_dbs(['postgres'])('lets you delete multiple fields and add fields', done => { + it('lets you delete multiple fields and add fields', done => { var obj1 = hasAllPODobject(); obj1.save() .then(() => { @@ -1757,17 +1757,70 @@ describe('schemas', () => { }); }); - it_exclude_dbs(['postgres'])('allows add indexes when you create a class', done => { + it('cannot create index if field does not exist', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { aString: 1}, + } + } + }, (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe('Field aString does not exist, cannot add index.'); + done(); + }); + }) + }); + + it('cannot create compound index if field does not exist', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: {type: 'String'} + }, + indexes: { + name1: { aString: 1, bString: 1}, + } + } + }, (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe('Field bString does not exist, cannot add index.'); + done(); + }); + }) + }); + + it('allows add index when you create a class', done => { request.post({ url: 'http://localhost:8378/1/schemas', headers: masterKeyHeaders, json: true, body: { className: "NewClass", + fields: { + aString: {type: 'String'} + }, indexes: { - name1: { field1: 1}, - name2: { field2: 1}, - } + name1: { aString: 1}, + }, } }, (error, response, body) => { expect(body).toEqual({ @@ -1777,21 +1830,49 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'} }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1}, - name2: { field2: 1}, + name1: { aString: 1}, }, }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toBe(3); + expect(indexes.length).toBe(2); done(); }); }); }); - it_exclude_dbs(['postgres'])('lets you add indexes', done => { + it('empty index returns nothing', done => { + request.post({ + url: 'http://localhost:8378/1/schemas', + headers: masterKeyHeaders, + json: true, + body: { + className: "NewClass", + fields: { + aString: {type: 'String'} + }, + indexes: {}, + } + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + aString: {type: 'String'} + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + done(); + }); + }); + + it('lets you add indexes', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -1803,9 +1884,12 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, body: { + fields: { + aString: {type: 'String'} + }, indexes: { - name1: { field1: 1 } - } + name1: { aString: 1}, + }, } }, (error, response, body) => { expect(dd(body, { @@ -1815,10 +1899,11 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'} }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, + name1: { aString: 1 }, } })).toEqual(undefined); request.get({ @@ -1833,10 +1918,11 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'} }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, + name1: { aString: 1 }, } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { @@ -1848,7 +1934,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('lets you add multiple indexes', done => { + it('lets you add multiple indexes', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -1860,10 +1946,16 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, body: { + fields: { + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + dString: {type: 'String'}, + }, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1, field4: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1, dString: 1 }, } } }, (error, response, body) => { @@ -1874,12 +1966,16 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + dString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1, field4: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1, dString: 1 }, } })).toEqual(undefined); request.get({ @@ -1894,12 +1990,16 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + dString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1, field4: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1, dString: 1 }, }, }); config.database.adapter.getIndexes('NewClass').then((indexes) => { @@ -1911,7 +2011,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('lets you delete indexes', done => { + it('lets you delete indexes', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -1923,8 +2023,11 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, body: { + fields: { + aString: {type: 'String'}, + }, indexes: { - name1: { field1: 1 } + name1: { aString: 1 }, } } }, (error, response, body) => { @@ -1935,10 +2038,11 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, + name1: { aString: 1 }, } })).toEqual(undefined); request.put({ @@ -1958,6 +2062,7 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, }); @@ -1970,7 +2075,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('lets you delete multiple indexes', done => { + it('lets you delete multiple indexes', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -1982,10 +2087,15 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, body: { + fields: { + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + }, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, } } }, (error, response, body) => { @@ -1996,12 +2106,15 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, } })).toEqual(undefined); request.put({ @@ -2022,10 +2135,13 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name3: { field3: 1 }, + name3: { cString: 1 }, } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { @@ -2037,7 +2153,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('lets you add and delete indexes', done => { + it('lets you add and delete indexes', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -2049,10 +2165,16 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, body: { + fields: { + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + dString: {type: 'String'}, + }, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, } } }, (error, response, body) => { @@ -2063,12 +2185,16 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + dString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name1: { field1: 1 }, - name2: { field2: 1 }, - name3: { field3: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, } })).toEqual(undefined); request.put({ @@ -2079,7 +2205,7 @@ describe('schemas', () => { indexes: { name1: { __op: 'Delete' }, name2: { __op: 'Delete' }, - name4: { field4: 1 }, + name4: { dString: 1 }, } } }, (error, response, body) => { @@ -2090,11 +2216,15 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, + aString: {type: 'String'}, + bString: {type: 'String'}, + cString: {type: 'String'}, + dString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, indexes: { - name3: { field3: 1 }, - name4: { field4: 1 }, + name3: { cString: 1 }, + name4: { dString: 1 }, } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { @@ -2106,7 +2236,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('cannot delete index that does not exist', done => { + it('cannot delete index that does not exist', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -2130,7 +2260,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('cannot update index that exist', done => { + it('cannot update index that exist', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -2142,8 +2272,11 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, body: { + fields: { + aString: {type: 'String'}, + }, indexes: { - name1: { field1: 1 } + name1: { aString: 1 } } } }, () => { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 238f2dcbf3..f62047467a 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -74,13 +74,9 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe } } - if (typeof indexes !== 'undefined') { + if (indexes && typeof indexes === 'object' && Object.keys(indexes).length > 0) { mongoObject._metadata = mongoObject._metadata || {}; - if (!indexes) { - delete mongoObject._metadata.indexes; - } else { - mongoObject._metadata.indexes = indexes; - } + mongoObject._metadata.indexes = indexes; } return mongoObject; @@ -173,7 +169,7 @@ export class MongoStorageAdapter { })); } - setIndexes(className, submittedIndexes, existingIndexes = {}) { + setIndexes(className, submittedIndexes, existingIndexes = {}, fields) { if (submittedIndexes === undefined) { return Promise.resolve(); } @@ -192,6 +188,11 @@ export class MongoStorageAdapter { deletePromises.push(promise); delete existingIndexes[name]; } else { + Object.keys(field).forEach(key => { + if (!fields.hasOwnProperty(key)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`); + } + }); existingIndexes[name] = field; insertedIndexes.push({ key: field, @@ -215,7 +216,7 @@ export class MongoStorageAdapter { schema = convertParseSchemaToMongoSchema(schema); const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes); mongoObject._id = className; - return this.setIndexes(className, schema.indexes, {}, schema) + return this.setIndexes(className, schema.indexes, {}, schema.fields) .then(() => this._schemaCollection()) .then(schemaCollection => schemaCollection._collection.insertOne(mongoObject)) .then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0])) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 689856d755..219c267daa 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -99,7 +99,7 @@ const toParseSchema = (schema) => { clps = {...emptyCLPS, ...schema.classLevelPermissions}; } let indexes = {}; - if (schema.classLevelPermissions) { + if (schema.indexes) { indexes = {...schema.indexes}; } return { @@ -602,22 +602,61 @@ export class PostgresStorageAdapter { }); } - setIndexes(className, submittedIndexes, existingIndexes = {}) { + setIndexes(className, submittedIndexes, existingIndexes = {}, fields, conn) { + conn = conn || this._client; if (submittedIndexes === undefined) { return Promise.resolve(); } - return this._ensureSchemaCollectionExists().then(() => { - const values = [className, 'schema', 'indexes', JSON.stringify(existingIndexes)] - return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values); + const deletedIndexes = []; + const insertedIndexes = []; + Object.keys(submittedIndexes).forEach(name => { + const field = submittedIndexes[name]; + if (existingIndexes[name] && field.__op !== 'Delete') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`); + } + if (!existingIndexes[name] && field.__op === 'Delete') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`); + } + if (field.__op === 'Delete') { + deletedIndexes.push(name); + delete existingIndexes[name]; + } else { + Object.keys(field).forEach(key => { + if (!fields.hasOwnProperty(key)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`); + } + }); + existingIndexes[name] = field; + insertedIndexes.push({ + key: field, + name, + }); + } }); + let insertPromise = Promise.resolve(); + if (insertedIndexes.length > 0) { + insertPromise = this.createIndexes(className, insertedIndexes, conn); + } + let deletePromise = Promise.resolve(); + if (deletedIndexes.length > 0) { + deletePromise = this.dropIndexes(className, deletedIndexes, conn); + } + return deletePromise + .then(() => insertPromise) + .then(() => this._ensureSchemaCollectionExists()) + .then(() => { + const values = [className, 'schema', 'indexes', JSON.stringify(existingIndexes)] + return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values); + }); } createClass(className, schema) { return this._client.tx(t => { const q1 = this.createTable(className, schema, t); const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', { className, schema }); + const q3 = this.setIndexes(className, schema.indexes, {}, schema.fields, t); - return t.batch([q1, q2]); + return t.batch([q1, q2, q3]); }) .then(() => { return toParseSchema(schema) @@ -1412,6 +1451,36 @@ export class PostgresStorageAdapter { console.error(error); }); } + + createIndexes(className, indexes, conn) { + conn = conn || this._client; + return conn.tx(t => { + const batch = []; + indexes.forEach((index) => { + const pattern = Object.keys(index.key).map((key) => `"${key}"`); + const qs = t.none(`CREATE INDEX ${index.name} ON "${className}" (${pattern.join(',')})`); + batch.push(qs); + }); + return t.batch(batch); + }) + } + + dropIndexes(className, indexes, conn) { + conn = conn || this._client; + return conn.tx(t => { + const batch = []; + indexes.forEach((index) => { + const qs = t.none(`DROP INDEX ${index}`); + batch.push(qs); + }); + return t.batch(batch); + }) + } + + getIndexes(className) { + const qs = `SELECT * FROM pg_indexes WHERE tablename = '${className}'`; + return this._client.any(qs, []); + } } function convertPolygonToSQL(polygon) { From 86aea28209b1a1bc320b6a75dfcfc0bbec9f7282 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 6 Oct 2017 16:32:55 -0500 Subject: [PATCH 05/11] full text compound indexes --- spec/ParseQuery.FullTextSearch.spec.js | 94 ++++++++++++------- .../Storage/Mongo/MongoStorageAdapter.js | 30 ++++-- .../Postgres/PostgresStorageAdapter.js | 20 ++-- 3 files changed, 89 insertions(+), 55 deletions(-) diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js index acb67c73f2..10cc8b2b26 100644 --- a/spec/ParseQuery.FullTextSearch.spec.js +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -31,7 +31,8 @@ const fullTextHelper = () => { const request = { method: "POST", body: { - subject: subjects[i] + subject: subjects[i], + comment: subjects[i], }, path: "/1/classes/TestObject" }; @@ -280,42 +281,69 @@ describe('Parse.Query Full Text Search testing', () => { }); describe_only_db('mongo')('Parse.Query Full Text Search testing', () => { - it('fullTextSearch: $search, only one text index', (done) => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - databaseAdapter: new MongoStorageAdapter({ uri: mongoURI }) + it('fullTextSearch: does not create text index if compound index exist', (done) => { + fullTextHelper().then(() => { + return databaseAdapter.dropAllIndexes('TestObject'); }).then(() => { - return rp.post({ - url: 'http://localhost:8378/1/batch', - body: { - requests: [ - { - method: "POST", - body: { - subject: "coffee is java" - }, - path: "/1/classes/TestObject" - }, - { - method: "POST", - body: { - subject: "java is coffee" - }, - path: "/1/classes/TestObject" + return databaseAdapter.getIndexes('TestObject'); + }).then((indexes) => { + expect(indexes.length).toEqual(1); + return databaseAdapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); + }).then(() => { + return databaseAdapter.getIndexes('TestObject'); + }).then((indexes) => { + expect(indexes.length).toEqual(2); + const where = { + subject: { + $text: { + $search: { + $term: 'coffee' } - ] - }, - json: true, + } + } + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, '_method': 'GET' }, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'test' } }); + }).then((resp) => { + expect(resp.results.length).toEqual(3); + return databaseAdapter.getIndexes('TestObject'); + }).then((indexes) => { + expect(indexes.length).toEqual(2); + done(); + }).catch(done.fail); + }); + + it('fullTextSearch: does not create text index if schema compound index exist', (done) => { + fullTextHelper().then(() => { + return databaseAdapter.dropAllIndexes('TestObject'); }).then(() => { - return databaseAdapter.createIndex('TestObject', {random: 'text'}); + return databaseAdapter.getIndexes('TestObject'); + }).then((indexes) => { + expect(indexes.length).toEqual(1); + return rp.put({ + url: 'http://localhost:8378/1/schemas/TestObject', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + 'X-Parse-Master-Key': 'test', + }, + body: { + indexes: { + text_test: { subject: 'text', comment: 'text'}, + }, + }, + }); }).then(() => { + return databaseAdapter.getIndexes('TestObject'); + }).then((indexes) => { + expect(indexes.length).toEqual(2); const where = { subject: { $text: { @@ -334,12 +362,12 @@ describe_only_db('mongo')('Parse.Query Full Text Search testing', () => { } }); }).then((resp) => { - fail(`Should not be more than one text index: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR); + expect(resp.results.length).toEqual(3); + return databaseAdapter.getIndexes('TestObject'); + }).then((indexes) => { + expect(indexes.length).toEqual(2); done(); - }); + }).catch(done.fail); }); it('fullTextSearch: $diacriticSensitive - false', (done) => { diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index f62047467a..609516a23c 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -401,7 +401,7 @@ export class MongoStorageAdapter { }, {}); readPreference = this._parseReadPreference(readPreference); - return this.createTextIndexesIfNeeded(className, query) + return this.createTextIndexesIfNeeded(className, query, schema) .then(() => this._adaptiveCollection(className)) .then(collection => collection.find(mongoWhere, { skip, @@ -503,20 +503,27 @@ export class MongoStorageAdapter { return Promise.resolve(); } - createTextIndexesIfNeeded(className, query) { + createTextIndexesIfNeeded(className, query, schema) { for(const fieldName in query) { if (!query[fieldName] || !query[fieldName].$text) { continue; } - const index = { - [fieldName]: 'text' + const existingIndexes = schema.indexes; + for (const key in existingIndexes) { + const index = existingIndexes[key]; + if (index.hasOwnProperty(fieldName)) { + return Promise.resolve(); + } + } + const indexName = `${fieldName}_text`; + const textIndex = { + [indexName]: { [fieldName]: 'text' } }; - return this.createIndex(className, index) + return this.setIndexes(className, textIndex, existingIndexes, schema.fields) .catch((error) => { - if (error.code === 85) { - throw new Parse.Error( - Parse.Error.INTERNAL_SERVER_ERROR, - 'Only one text index is supported, please delete all text indexes to use new field.'); + if (error.code === 85) { // Index exist with different options + return this.getIndexes(className) + .then((indexes) => this.setIndexes(className, {}, indexes, schema.fields)); } throw error; }); @@ -533,6 +540,11 @@ export class MongoStorageAdapter { return this._adaptiveCollection(className) .then(collection => collection._mongoCollection.dropIndex(index)); } + + dropAllIndexes(className) { + return this._adaptiveCollection(className) + .then(collection => collection._mongoCollection.dropIndexes()); + } } export default MongoStorageAdapter; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 219c267daa..2b12e7651f 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1457,29 +1457,23 @@ export class PostgresStorageAdapter { return conn.tx(t => { const batch = []; indexes.forEach((index) => { - const pattern = Object.keys(index.key).map((key) => `"${key}"`); - const qs = t.none(`CREATE INDEX ${index.name} ON "${className}" (${pattern.join(',')})`); + const fieldNames = Object.keys(index.key); + const pattern = fieldNames.map((key, index) => `$${index + 3}:name`); + const qs = t.none(`CREATE INDEX $1:name ON $2:name (${pattern.join(',')})`, [index.name, className, ...fieldNames]); batch.push(qs); }); return t.batch(batch); - }) + }); } dropIndexes(className, indexes, conn) { conn = conn || this._client; - return conn.tx(t => { - const batch = []; - indexes.forEach((index) => { - const qs = t.none(`DROP INDEX ${index}`); - batch.push(qs); - }); - return t.batch(batch); - }) + return conn.tx(t => t.batch(indexes.map(i => t.none('DROP INDEX $1:name', i)))); } getIndexes(className) { - const qs = `SELECT * FROM pg_indexes WHERE tablename = '${className}'`; - return this._client.any(qs, []); + const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}'; + return this._client.any(qs, {className}); } } From ec2c0cba0720dbc6fdf7657f4b9e4b1330ec7263 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 10 Oct 2017 19:15:11 -0500 Subject: [PATCH 06/11] pg clean up --- .../Postgres/PostgresStorageAdapter.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 2b12e7651f..35ac5ddd99 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -646,7 +646,7 @@ export class PostgresStorageAdapter { .then(() => this._ensureSchemaCollectionExists()) .then(() => { const values = [className, 'schema', 'indexes', JSON.stringify(existingIndexes)] - return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values); + return conn.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values); }); } @@ -1453,22 +1453,13 @@ export class PostgresStorageAdapter { } createIndexes(className, indexes, conn) { - conn = conn || this._client; - return conn.tx(t => { - const batch = []; - indexes.forEach((index) => { - const fieldNames = Object.keys(index.key); - const pattern = fieldNames.map((key, index) => `$${index + 3}:name`); - const qs = t.none(`CREATE INDEX $1:name ON $2:name (${pattern.join(',')})`, [index.name, className, ...fieldNames]); - batch.push(qs); - }); - return t.batch(batch); - }); + return (conn || this._client).tx(t => t.batch(indexes.map(i => { + return t.none('CREATE INDEX $1:name ON $2:name ($3:name)', [i.name, className, i.key]); + }))); } dropIndexes(className, indexes, conn) { - conn = conn || this._client; - return conn.tx(t => t.batch(indexes.map(i => t.none('DROP INDEX $1:name', i)))); + return (conn || this._client).tx(t => t.batch(indexes.map(i => t.none('DROP INDEX $1:name', i)))); } getIndexes(className) { From 2904de380a7aa415bbd49e12ebdf1fc0f45cf786 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 31 Oct 2017 20:53:18 -0500 Subject: [PATCH 07/11] get indexes on startup --- spec/schemas.spec.js | 20 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 19 ++++++++++++++++++ .../Postgres/PostgresStorageAdapter.js | 4 ++++ src/Controllers/DatabaseController.js | 4 +++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index f5dca7d7db..0e49005f57 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2297,4 +2297,24 @@ describe('schemas', () => { }); }) }); + + it_exclude_dbs(['postgres'])('get indexes on startup', (done) => { + const obj = new Parse.Object('TestObject'); + obj.save().then(() => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + }).then(() => { + request.get({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + done(); + }); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 609516a23c..df5ad2b5a3 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -545,6 +545,25 @@ export class MongoStorageAdapter { return this._adaptiveCollection(className) .then(collection => collection._mongoCollection.dropIndexes()); } + + updateSchemaWithIndexes() { + return this.getAllClasses() + .then((classes) => { + const promises = classes.map((schema) => { + return this.getIndexes(schema.className).then((indexes) => { + indexes = indexes.reduce((obj, index) => { + obj[index.name] = index.key; + return obj; + }, {}); + return this._schemaCollection() + .then(schemaCollection => schemaCollection.updateSchema(schema.className, { + $set: { _metadata: { indexes: indexes } } + })); + }); + }); + return Promise.all(promises); + }); + } } export default MongoStorageAdapter; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 35ac5ddd99..e932b9ff8d 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1466,6 +1466,10 @@ export class PostgresStorageAdapter { const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}'; return this._client.any(qs, {className}); } + + updateSchemaWithIndexes() { + return Promise.resolve(); + } } function convertPolygonToSQL(polygon) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c1cdfdadca..83524aa179 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1005,9 +1005,11 @@ DatabaseController.prototype.performInitialization = function() { throw error; }); + const indexPromise = this.adapter.updateSchemaWithIndexes(); + // Create tables for volatile classes const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas }); - return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit]); + return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit, indexPromise]); } function joinTableName(className, key) { From bf055fd89469c702e84365ce4047484780c1f2ba Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Wed, 15 Nov 2017 21:28:04 -0600 Subject: [PATCH 08/11] test compound index on startup --- spec/schemas.spec.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index da16510526..eaf1a6f087 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2317,4 +2317,29 @@ describe('schemas', () => { }); }); }); + + it_exclude_dbs(['postgres'])('get compound indexes on startup', (done) => { + const obj = new Parse.Object('TestObject'); + obj.set('subject', 'subject'); + obj.set('comment', 'comment'); + obj.save().then(() => { + return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); + }).then(() => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + }).then(() => { + request.get({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + expect(body.indexes.subject_text_comment_text).toBeDefined(); + done(); + }); + }); + }); }); From e251d44f4909ce9bd7fcb61f59258d1476c04f25 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 21 Nov 2017 17:56:55 -0600 Subject: [PATCH 09/11] add default _id to index, full Text index on startup --- spec/ParseQuery.FullTextSearch.spec.js | 32 +++++++++++- spec/schemas.spec.js | 18 ++++++- .../Storage/Mongo/MongoStorageAdapter.js | 49 ++++++++++++------- .../Postgres/PostgresStorageAdapter.js | 8 ++- src/Controllers/SchemaController.js | 2 +- 5 files changed, 85 insertions(+), 24 deletions(-) diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js index 10cc8b2b26..2563781d6e 100644 --- a/spec/ParseQuery.FullTextSearch.spec.js +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -315,7 +315,21 @@ describe_only_db('mongo')('Parse.Query Full Text Search testing', () => { return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { expect(indexes.length).toEqual(2); - done(); + rp.get({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + expect(body.indexes._id_._id).toEqual(1); + expect(body.indexes.subject_text_comment_text).toBeDefined(); + expect(body.indexes.subject_text_comment_text.subject).toEqual('text'); + expect(body.indexes.subject_text_comment_text.comment).toEqual('text'); + done(); + }); }).catch(done.fail); }); @@ -366,7 +380,21 @@ describe_only_db('mongo')('Parse.Query Full Text Search testing', () => { return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { expect(indexes.length).toEqual(2); - done(); + rp.get({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + expect(body.indexes._id_._id).toEqual(1); + expect(body.indexes.text_test).toBeDefined(); + expect(body.indexes.text_test.subject).toEqual('text'); + expect(body.indexes.text_test.comment).toEqual('text'); + done(); + }); }).catch(done.fail); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index eaf1a6f087..f49bca381c 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1903,6 +1903,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, } })).toEqual(undefined); @@ -1922,6 +1923,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, } }); @@ -1973,6 +1975,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, name2: { bString: 1 }, name3: { cString: 1, dString: 1 }, @@ -1997,6 +2000,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, name2: { bString: 1 }, name3: { cString: 1, dString: 1 }, @@ -2042,6 +2046,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, } })).toEqual(undefined); @@ -2065,6 +2070,9 @@ describe('schemas', () => { aString: {type: 'String'}, }, classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { expect(indexes.length).toEqual(1); @@ -2112,6 +2120,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, name2: { bString: 1 }, name3: { cString: 1 }, @@ -2141,6 +2150,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name3: { cString: 1 }, } }); @@ -2192,6 +2202,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1 }, name2: { bString: 1 }, name3: { cString: 1 }, @@ -2223,6 +2234,7 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name3: { cString: 1 }, name4: { dString: 1 }, } @@ -2323,7 +2335,7 @@ describe('schemas', () => { obj.set('subject', 'subject'); obj.set('comment', 'comment'); obj.save().then(() => { - return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); + return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 1}); }).then(() => { return reconfigureServer({ appId: 'test', @@ -2336,8 +2348,12 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, }, (error, response, body) => { + console.log(body); expect(body.indexes._id_).toBeDefined(); + expect(body.indexes._id_._id).toEqual(1); expect(body.indexes.subject_text_comment_text).toBeDefined(); + expect(body.indexes.subject_text_comment_text.subject).toEqual('text'); + expect(body.indexes.subject_text_comment_text.comment).toEqual('text'); done(); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index afd62a52a0..72d72a5391 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -170,10 +170,13 @@ export class MongoStorageAdapter { })); } - setIndexes(className, submittedIndexes, existingIndexes = {}, fields) { + setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields) { if (submittedIndexes === undefined) { return Promise.resolve(); } + if (Object.keys(existingIndexes).length === 0) { + existingIndexes = { _id_: { _id: 1} }; + } const deletePromises = []; const insertedIndexes = []; Object.keys(submittedIndexes).forEach(name => { @@ -213,11 +216,34 @@ export class MongoStorageAdapter { })); } + setIndexesFromMongo(className) { + return this.getIndexes(className).then((indexes) => { + indexes = indexes.reduce((obj, index) => { + if (index.key._fts) { + delete index.key._fts; + delete index.key._ftsx; + for (const field in index.weights) { + index.key[field] = 'text'; + } + } + obj[index.name] = index.key; + return obj; + }, {}); + return this._schemaCollection() + .then(schemaCollection => schemaCollection.updateSchema(className, { + $set: { _metadata: { indexes: indexes } } + })); + }).catch(() => { + // Ignore if collection not found + return Promise.resolve(); + }); + } + createClass(className, schema) { schema = convertParseSchemaToMongoSchema(schema); const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes); mongoObject._id = className; - return this.setIndexes(className, schema.indexes, {}, schema.fields) + return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields) .then(() => this._schemaCollection()) .then(schemaCollection => schemaCollection._collection.insertOne(mongoObject)) .then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0])) @@ -541,11 +567,10 @@ export class MongoStorageAdapter { const textIndex = { [indexName]: { [fieldName]: 'text' } }; - return this.setIndexes(className, textIndex, existingIndexes, schema.fields) + return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields) .catch((error) => { if (error.code === 85) { // Index exist with different options - return this.getIndexes(className) - .then((indexes) => this.setIndexes(className, {}, indexes, schema.fields)); + return this.setIndexesFromMongo(className); } throw error; }); @@ -572,19 +597,7 @@ export class MongoStorageAdapter { return this.getAllClasses() .then((classes) => { const promises = classes.map((schema) => { - return this.getIndexes(schema.className).then((indexes) => { - indexes = indexes.reduce((obj, index) => { - obj[index.name] = index.key; - return obj; - }, {}); - return this._schemaCollection() - .then(schemaCollection => schemaCollection.updateSchema(schema.className, { - $set: { _metadata: { indexes: indexes } } - })); - }).catch(() => { - // Ignore if collection not found - return Promise.resolve(); - }); + return this.setIndexesFromMongo(schema.className); }); return Promise.all(promises); }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 4f83f4b6ba..1efc71c451 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -606,11 +606,14 @@ export class PostgresStorageAdapter { }); } - setIndexes(className, submittedIndexes, existingIndexes = {}, fields, conn) { + setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields, conn) { conn = conn || this._client; if (submittedIndexes === undefined) { return Promise.resolve(); } + if (Object.keys(existingIndexes).length === 0) { + existingIndexes = { _id_: { _id: 1} }; + } const deletedIndexes = []; const insertedIndexes = []; Object.keys(submittedIndexes).forEach(name => { @@ -658,7 +661,7 @@ export class PostgresStorageAdapter { return this._client.tx(t => { const q1 = this.createTable(className, schema, t); const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', { className, schema }); - const q3 = this.setIndexes(className, schema.indexes, {}, schema.fields, t); + const q3 = this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields, t); return t.batch([q1, q2, q3]); }) @@ -1251,6 +1254,7 @@ export class PostgresStorageAdapter { } find(className, schema, query, { skip, limit, sort, keys }) { + console.log('find'); debug('find', className, query, {skip, limit, sort, keys }); const hasLimit = limit !== undefined; const hasSkip = skip !== undefined; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 4de547ce10..ab579b6aa2 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -538,7 +538,7 @@ export default class SchemaController { return Promise.all(promises); }) .then(() => this.setPermissions(className, classLevelPermissions, newSchema)) - .then(() => this._dbAdapter.setIndexes(className, indexes, schema.indexes, newSchema)) + .then(() => this._dbAdapter.setIndexesWithSchemaFormat(className, indexes, schema.indexes, newSchema)) .then(() => this.reloadData({ clearCache: true })) //TODO: Move this logic into the database adapter .then(() => { From 95fd3856881c600b0f94b11fd6ef634c913d402c Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 21 Nov 2017 18:16:01 -0600 Subject: [PATCH 10/11] lint --- spec/schemas.spec.js | 3 +-- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index f49bca381c..8c7481d9bb 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2335,7 +2335,7 @@ describe('schemas', () => { obj.set('subject', 'subject'); obj.set('comment', 'comment'); obj.save().then(() => { - return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 1}); + return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); }).then(() => { return reconfigureServer({ appId: 'test', @@ -2348,7 +2348,6 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, }, (error, response, body) => { - console.log(body); expect(body.indexes._id_).toBeDefined(); expect(body.indexes._id_._id).toEqual(1); expect(body.indexes.subject_text_comment_text).toBeDefined(); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 1efc71c451..971b19ac7e 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1254,7 +1254,6 @@ export class PostgresStorageAdapter { } find(className, schema, query, { skip, limit, sort, keys }) { - console.log('find'); debug('find', className, query, {skip, limit, sort, keys }); const hasLimit = limit !== undefined; const hasSkip = skip !== undefined; From 102af94a27c3df1ac3b12cb79f4a7f64b053ab29 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 21 Nov 2017 18:30:32 -0600 Subject: [PATCH 11/11] fix test --- spec/Schema.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 9bdb253a13..986740650b 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -304,6 +304,9 @@ describe('SchemaController', () => { fooNineteen: {type: 'String'}, }, classLevelPermissions: { ...levelPermissions }, + indexes: { + _id_: { _id: 1 } + } }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined);