diff --git a/src/migrations.js b/src/migrations.js index 22135141..3deefa1c 100644 --- a/src/migrations.js +++ b/src/migrations.js @@ -46,37 +46,30 @@ function getMigrations(schema) { version: '2', previous: '0.1.0', install: [ - `CREATE TYPE ${schema}.job_state AS ENUM ( - 'created', - 'retry', - 'active', - 'complete', - 'expired', - 'cancelled' - )`, + `CREATE TYPE ${schema}.job_state AS ENUM ('created','retry','active','complete','expired','cancelled')`, `ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE ${schema}.job_state USING state::${schema}.job_state`, `ALTER TABLE ${schema}.job DROP CONSTRAINT job_singleton`, `ALTER TABLE ${schema}.job ADD singletonKey text`, - `CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'complete'`, - `CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'complete' AND singletonKey IS NULL`, `CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' AND singletonOn IS NULL`, + `CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL`, + `CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'`, // migrate data to use retry state `UPDATE ${schema}.job SET state = 'retry' WHERE state = 'expired' AND retryCount < retryLimit`, - // expired jobs weren't being archived in prev schema -- no rollback for this :) + // expired jobs weren't being archived in prev schema `UPDATE ${schema}.job SET completedOn = now() WHERE state = 'expired' and retryLimit = retryCount`, // just using good ole fashioned completedOn `ALTER TABLE ${schema}.job DROP COLUMN expiredOn` ], uninstall: [ + `ALTER TABLE ${schema}.job ADD expiredOn timestamp without time zone`, + `DROP INDEX ${schema}.job_singletonKey`, `DROP INDEX ${schema}.job_singletonOn`, `DROP INDEX ${schema}.job_singletonKeyOn`, - `DROP INDEX ${schema}.job_singletonKey`, `ALTER TABLE ${schema}.job DROP COLUMN singletonKey`, `ALTER TABLE ${schema}.job ALTER COLUMN state SET DATA TYPE text`, `DROP TYPE ${schema}.job_state`, // restoring prev unique constraint `ALTER TABLE ${schema}.job ADD CONSTRAINT job_singleton UNIQUE(name, singletonOn)`, - `ALTER TABLE ${schema}.job ADD expiredOn timestamp without time zone`, // roll retry state back to expired `UPDATE ${schema}.job SET state = 'expired' where state = 'retry'` ] diff --git a/src/plans.js b/src/plans.js index 2ee0875a..cf2e0167 100644 --- a/src/plans.js +++ b/src/plans.js @@ -37,6 +37,8 @@ function createVersionTable(schema) { } function createJobStateEnum(schema) { + // ENUM definition order is important + // base type is numeric and first values are less than last values return ` CREATE TYPE ${schema}.job_state AS ENUM ( 'created', @@ -67,22 +69,24 @@ function createJobTable(schema) { )`; } - -function createIndexSingletonOn(schema){ +function createIndexSingletonKey(schema){ + // anything with singletonKey means "only 1 job can be queued or active at a time" return ` - CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' + CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' AND singletonOn IS NULL `; } -function createIndexSingletonKeyOn(schema){ +function createIndexSingletonOn(schema){ + // anything with singletonOn means "only 1 job within this time period, queued, active or completed" return ` - CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired' + CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL `; } -function createIndexSingletonKey(schema){ +function createIndexSingletonKeyOn(schema){ + // anything with both singletonOn and singletonKey means "only 1 job within this time period with this key, queued, active or completed" return ` - CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' + CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired' `; } diff --git a/test/migrationTest.js b/test/migrationTest.js index 247364fc..f22285d5 100644 --- a/test/migrationTest.js +++ b/test/migrationTest.js @@ -15,28 +15,65 @@ describe('migration', function() { }); it('should migrate to previous version and back again', function (finished) { - this.timeout(3000); contractor.create() .then(() => db.migrate(currentSchemaVersion, 'remove')) + .then(version => { + assert.notEqual(version, currentSchemaVersion); + return db.migrate(version); + }) + .then(version => { + assert.equal(version, currentSchemaVersion); + finished(); + }); + }); + + it('should migrate to latest during start if on previous schema version', function(finished){ + + contractor.create() + .then(() => db.migrate(currentSchemaVersion, 'remove')) + .then(() => new PgBoss(helper.config).start()) .then(() => contractor.version()) .then(version => { - assert(version); + assert.equal(version, currentSchemaVersion); + finished(); + }); + }); + + it('should migrate through 2 versions back and forth', function (finished) { + + let prevVersion; + + contractor.create() + .then(() => db.migrate(currentSchemaVersion, 'remove')) + .then(version => { + prevVersion = version; assert.notEqual(version, currentSchemaVersion); + return db.migrate(version, 'remove'); + }) + .then(version => { + assert.notEqual(version, prevVersion); + + return db.migrate(version); + }) + .then(version => { + assert.equal(version, prevVersion); + return db.migrate(version); }) - .then(() => contractor.version()) .then(version => { assert.equal(version, currentSchemaVersion); finished(); }); }); - it('should migrate to latest during start if on previous schema version', function(finished){ + + it('should migrate to latest during start if on previous 2 schema versions', function(finished){ contractor.create() .then(() => db.migrate(currentSchemaVersion, 'remove')) + .then(version => db.migrate(version, 'remove')) .then(() => new PgBoss(helper.config).start()) .then(() => contractor.version()) .then(version => {