Knex: Promises don't behave as expected in migrations

Created on 16 Jun 2014  ·  10Comments  ·  Source: knex/knex

(This may be a duplicate of #312)

A migration file like so:

exports.up = function(knex, Promise) {
  var first = knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  });

  var second = first.then(function() {
    return knex.schema.createTable('second', function(table) {
      table.increments('id');
      table.string('name');
    });
  });

  return Promise.all([first, second]);
};

exports.down = function(knex, Promise) {

};

gives the following output:

{ __cid: '__cid1',
  sql: 'create table "first" ("id" serial primary key, "name" varchar(255))',
  bindings: [] }
{ __cid: '__cid2',
  sql: 'create table "first" ("id" serial primary key, "name" varchar(255))',
  bindings: [] }
{ __cid: '__cid3',
  sql: 'create table "second" ("id" serial primary key, "name" varchar(255))',
  bindings: [] }
error: duplicate key value violates unique constraint "pg_type_typname_nsp_index"
    at Connection.parseE (/home/sohum/node_modules/pg/lib/connection.js:526:11)
    at Connection.parseMessage (/home/sohum/node_modules/pg/lib/connection.js:356:17)
    at Socket.<anonymous> (/home/sohum/node_modules/pg/lib/connection.js:105:22)
    at Socket.EventEmitter.emit (events.js:95:17)
    at Socket.<anonymous> (_stream_readable.js:745:14)
    at Socket.EventEmitter.emit (events.js:92:17)
    at emitReadable_ (_stream_readable.js:407:10)
    at emitReadable (_stream_readable.js:403:5)
    at readableAddChunk (_stream_readable.js:165:9)
    at Socket.Readable.push (_stream_readable.js:127:10)
question

Most helpful comment

So the confusion here is that the createTable method does not return a promise, but rather it returns a SchemaBuilder object, which is a "thenable", i.e. calling .then on the object will return a valid A+ promise, but the object its self is not a promise.

This was done specifically to allow you to use the syntax:

  return knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  })
  .createTable('second', function(table) {
      table.increments('id');
      table.string('name');
  }).then(function() {
    // all done
  });

which should run the migrations as expected on the same connection in sequence.

Also, there is no need for Promise.all here, as this would accomplish the same:

exports.up = function(knex, Promise) {
  return knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  }).then(function() {
    return knex.schema.createTable('second', function(table) {
      table.increments('id');
      table.string('name');
    });
  });
};

though if you wanted, you could do:

exports.up = function(knex, Promise) {
  var first = knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  }).then(); // coerce thenable to a promise

  var second = first.then(function() {
    return knex.schema.createTable('second', function(table) {
      table.increments('id');
      table.string('name');
    });
  });

  return Promise.all([first, second]);
};

and things would work as defined by the spec.

All 10 comments

I don't think that you should return Promise.all([first, second]);, since you already chained second to first. Could you please try to only return first? Since your second createTable is inside a then which is chained off of first, there is no need to add it to Promise.all, since it will get executed anyway while first is being resolved.

The promise spec says that once a promise is fulfilled, it should never be fulfilled again, and all future invocations of then should return the cached result. If first and second are both fulfilled, then Promise.all([first, second]) should be ~ a no-op.

return first has the same behaviour - i.e., it tries to create the first, first, and second tables, which is _even more wrong_, as there should never be any code that actually evaluates anything inside the function passed to first.then in that scenario.

Uhm ok that does sound weird. Not using Promise.all could give a small boost in performance though ;)

But yeah, this does seem to be wrong. I'll mark this as a bug for now and wait until @tgriesser chimes in. Thanks for reporting!

Haha, I don't particularly care about one tick's worth of performance in my migrations :p

Sorry, I made a wrong statement above - return first should still create the second table, it's just that the result of calling .then on the result of the function won't wait for it. Which, w/e - it should still definitely not do first, first, second!

Not sure I fully understand: Why wouldn't the 2nd table wait for the creation of the first one? Since you chain off of the first promise using then they will be effectively run in series. That should not be an issue.

The second table's creation will, yes, but the result of the exports.up function won't. I.e., if you call exports.up().then, it's the same as calling first.then and not second.then - because it _is_ first.then!

Ah, misunderstood you there - yeah you're right about that!

So the confusion here is that the createTable method does not return a promise, but rather it returns a SchemaBuilder object, which is a "thenable", i.e. calling .then on the object will return a valid A+ promise, but the object its self is not a promise.

This was done specifically to allow you to use the syntax:

  return knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  })
  .createTable('second', function(table) {
      table.increments('id');
      table.string('name');
  }).then(function() {
    // all done
  });

which should run the migrations as expected on the same connection in sequence.

Also, there is no need for Promise.all here, as this would accomplish the same:

exports.up = function(knex, Promise) {
  return knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  }).then(function() {
    return knex.schema.createTable('second', function(table) {
      table.increments('id');
      table.string('name');
    });
  });
};

though if you wanted, you could do:

exports.up = function(knex, Promise) {
  var first = knex.schema.createTable('first', function(table) {
    table.increments('id');
    table.string('name');
  }).then(); // coerce thenable to a promise

  var second = first.then(function() {
    return knex.schema.createTable('second', function(table) {
      table.increments('id');
      table.string('name');
    });
  });

  return Promise.all([first, second]);
};

and things would work as defined by the spec.

Ok that explains that. Didn't know that - sorry about the confusion for not being able to give a straight answer right away @SohumB

Yea, we have more complicated dependency graphs than that; this was just a minimal test case! Thanks for the help.

@johanneslumpe

sohum@diurnal ~ % cat test.js
var Promise = require('bluebird');

function promises() {
  var first = Promise.resolve('wee');
  var second = first.then(function() {
    console.log('delayin');
    return Promise.delay(1000);
  }).then(function() {
    console.log('done!');
  });
  return first;
}

promises().then(function() { console.log('yep, first is finished'); });

sohum@diurnal ~ % node test.js
delayin
yep, first is finished
done!
Was this page helpful?
0 / 5 - 0 ratings

Related issues

koskimas picture koskimas  ·  3Comments

saurabhghewari picture saurabhghewari  ·  3Comments

tjwebb picture tjwebb  ·  3Comments

sandrocsimas picture sandrocsimas  ·  3Comments

mishitpatel picture mishitpatel  ·  3Comments