diff --git a/spec/PostgresStorageAdapter.spec.js b/spec/PostgresStorageAdapter.spec.js index f4571208f8..df4c5478a4 100644 --- a/spec/PostgresStorageAdapter.spec.js +++ b/spec/PostgresStorageAdapter.spec.js @@ -1,6 +1,10 @@ import PostgresStorageAdapter from '../src/Adapters/Storage/Postgres/PostgresStorageAdapter'; const databaseURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; +const getColumns = (client, className) => { + return client.map('SELECT column_name FROM information_schema.columns WHERE table_name = $', { className }, a => a.column_name); +}; + describe_only_db('postgres')('PostgresStorageAdapter', () => { beforeEach(done => { const adapter = new PostgresStorageAdapter({ uri: databaseURI }) @@ -19,4 +23,100 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { expect(adapter._client.$pool.ending).toEqual(true); done(); }); + + it('schemaUpgrade, upgrade the database schema when schema changes', done => { + const adapter = new PostgresStorageAdapter({ uri: databaseURI }); + const client = adapter._client; + const className = '_PushStatus'; + const schema = { + fields: { + "pushTime": { type: 'String' }, + "source": { type: 'String' }, + "query": { type: 'String' }, + }, + }; + + adapter.createTable(className, schema) + .then(() => getColumns(client, className)) + .then(columns => { + expect(columns).toContain('pushTime'); + expect(columns).toContain('source'); + expect(columns).toContain('query'); + expect(columns).not.toContain('expiration_interval'); + + schema.fields.expiration_interval = { type:'Number' }; + return adapter.schemaUpgrade(className, schema); + }) + .then(() => getColumns(client, className)) + .then(columns => { + expect(columns).toContain('pushTime'); + expect(columns).toContain('source'); + expect(columns).toContain('query'); + expect(columns).toContain('expiration_interval'); + done(); + }) + .catch(error => done.fail(error)); + }); + + it('schemaUpgrade, maintain correct schema', done => { + const adapter = new PostgresStorageAdapter({ uri: databaseURI }); + const client = adapter._client; + const className = 'Table'; + const schema = { + fields: { + "columnA": { type: 'String' }, + "columnB": { type: 'String' }, + "columnC": { type: 'String' }, + }, + }; + + adapter.createTable(className, schema) + .then(() => getColumns(client, className)) + .then(columns => { + expect(columns).toContain('columnA'); + expect(columns).toContain('columnB'); + expect(columns).toContain('columnC'); + + return adapter.schemaUpgrade(className, schema); + }) + .then(() => getColumns(client, className)) + .then(columns => { + expect(columns.length).toEqual(3); + expect(columns).toContain('columnA'); + expect(columns).toContain('columnB'); + expect(columns).toContain('columnC'); + done(); + }) + .catch(error => done.fail(error)); + }); + + it('Create a table without columns and upgrade with columns', done => { + const adapter = new PostgresStorageAdapter({ uri: databaseURI }); + const client = adapter._client; + const className = 'EmptyTable'; + let schema = {}; + + adapter.createTable(className, schema) + .then(() => getColumns(client, className)) + .then(columns => { + expect(columns.length).toBe(0); + + schema = { + fields: { + "columnA": { type: 'String' }, + "columnB": { type: 'String' } + }, + }; + + return adapter.schemaUpgrade(className, schema); + }) + .then(() => getColumns(client, className)) + .then(columns => { + expect(columns.length).toEqual(2); + expect(columns).toContain('columnA'); + expect(columns).toContain('columnB'); + done(); + }) + .catch(error => done.fail(error)); + }) }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 168e452c2c..e46fcec73b 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -750,11 +750,27 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } - addFieldIfNotExists(className: string, fieldName: string, type: any) { + schemaUpgrade(className: string, schema: SchemaType, conn: any) { + debug('schemaUpgrade', { className, schema }); + conn = conn || this._client; + const self = this; + + return conn.tx('schema-upgrade', function * (t) { + const columns = yield t.map('SELECT column_name FROM information_schema.columns WHERE table_name = $', { className }, a => a.column_name); + const newColumns = Object.keys(schema.fields) + .filter(item => columns.indexOf(item) === -1) + .map(fieldName => self.addFieldIfNotExists(className, fieldName, schema.fields[fieldName], t)); + + yield t.batch(newColumns); + }); + } + + addFieldIfNotExists(className: string, fieldName: string, type: any, conn: any) { // TODO: Must be revised for invalid logic... debug('addFieldIfNotExists', {className, fieldName, type}); + conn = conn || this._client; const self = this; - return this._client.tx('add-field-if-not-exists', function * (t) { + return conn.tx('add-field-if-not-exists', function * (t) { if (type.type !== 'Relation') { try { yield t.none('ALTER TABLE $ ADD COLUMN $ $', { @@ -1591,14 +1607,17 @@ export class PostgresStorageAdapter implements StorageAdapter { } performInitialization({ VolatileClassesSchemas }: any) { + // TODO: This method needs to be rewritten to make proper use of connections (@vitaly-t) debug('performInitialization'); const promises = VolatileClassesSchemas.map((schema) => { - return this.createTable(schema.className, schema).catch((err) => { - if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) { - return Promise.resolve(); - } - throw err; - }); + return this.createTable(schema.className, schema) + .catch((err) => { + if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) { + return Promise.resolve(); + } + throw err; + }) + .then(() => this.schemaUpgrade(schema.className, schema)); }); return Promise.all(promises) .then(() => {