Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migrating from multiple files #993

Closed
Pomax opened this issue Sep 23, 2015 · 35 comments
Closed

migrating from multiple files #993

Pomax opened this issue Sep 23, 2015 · 35 comments

Comments

@Pomax
Copy link

Pomax commented Sep 23, 2015

How do I ensure that knex migrates using multiple files in a reliable order, making sure to finish one file before moving on to the next? #148 suggests this the file processing order is based on file timestamp which would be insane, since that makes edits to files in order to fix isues with them (typos, refactors, etc) change the actual migration order. Is there an explicit ordering for files in a migration directory, or a way to specify them? I can't find anything about this in the docs.

@rhys-vdw
Copy link
Member

The files are read in name order, and they are named: [timestamp]_[migration name]. So not by the file system's last modified timestamp. Editing files wont change their order unless you rename them (which you ought not do if they've been run).

@rhys-vdw
Copy link
Member

Please close this issue if this answers your question. I'll leave #563 and #992 open.

@ricardograca
Copy link
Member

@Pomax It is considered a very bad practice to change migration files that have already been published and potentially used by other people. The correct approach is to create a new migration that introduces the needed changes.

Migrations aren't the strongest point of knex unfortunately. Ideally you should also have a mechanism to recreate a database schema from a schema definition file, and not have to rely on migrations to always be the source of the schema.

@rhys-vdw
Copy link
Member

There are some deficiencies with the CLI, but personally I think they're superior to a schema file. Each to their own.

@ricardograca
Copy link
Member

@rhys-vdw I'm saying there should be a schema file and migrations. Migrations for quick and small changes of already existing databases, and the schema file for new installations.

It's not very efficient to keep migrations around for a very long time if the schema has changed considerably over time and a lot of them are needed to recreate it correctly.

@Pomax
Copy link
Author

Pomax commented Sep 23, 2015

I tried this with two files, 1_... and 2_... but by the time 2 gets run, the changes that 1 sets up have not been finalised yet, suggesting that rather than migrations, the whole set is treated as a single huge migration. Am I misunderstanding this? (both files expose an .up and .down)

@ricardograca
Copy link
Member

Can you show us your migration files' contents?

@Pomax
Copy link
Author

Pomax commented Sep 23, 2015

@Pomax
Copy link
Author

Pomax commented Sep 23, 2015

On a secondary note, I'm going to borrow a page out of Mike Hoye's book when it comes to documentation, for the earlier explanation on how migrations work: that is exactly the information needed.

Instead of writing that information as an issue comment and asking to close this issue, please put that brief explanation in the migration section of the documentation: "to tell knex where the directory with migration files are, add .... to your knexfile. Files in your migration directory must be named using a timestap_arbitraryname.js naming convention, and files are sequentially parsed, older timestamp first". Boom, done, thousands of people can now find that information instead of having it live hidden away in a git issue reply.

@ricardograca
Copy link
Member

Right, another undocumented feature of migrations is that the second argument to your up and down methods is a bluebird instance, so you're better off writing your migrations as:

exports.up = function(knex, Promise) {
  return Promise.all([
    knex.schema.createTable('publishedProjects', function(t) {
      t.increments('id');
      t.text('title').notNullable();
      t.text('tags');
      t.text('description');
    }),
    knex.schema //...
  ])
}

@ricardograca
Copy link
Member

I'm not sure what you mean by use ...., but you can submit a pull-request with the missing information. It's not like anybody doesn't want to document this stuff, it's more a lack of time than anything else.

@Pomax
Copy link
Author

Pomax commented Sep 23, 2015

that was just a placeholder. I'll happily submit a PR, but clearly @rhys-vdw already knew this and was in a great spot to preempt by filing that PR first =)

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

I switched over the two files to use Promise.all, but I'm still getting the same deal where up is called in the second file, but the table it needs to perform a migration on is not actually available.

Knexfile:

var path = require('path');

if (!process.env.NODE_ENV) {
  require('habitat').load(".env");
} else if (process.env.NODE_ENV === 'test') {
  require('habitat').load('tests.env');
}

module.exports = {
  development: {
    client: 'pg',
    debug: process.env.DEBUG == true,
    connection: process.env.DATABASE_URL,
    migrations: {
      directory: path.resolve(__dirname, 'migrations'),
      tableName: 'migrations'
    }
  }
};

(The DATABASE_URL variable is set to postgres:///publish regardless of the env loaded in, and exists as postgres database)

new file 1: ./migrations/1_create_users_projects_files.js, https://pastebin.mozilla.org/8847464
new file 2: ./migrations/2_alter_project_table.js, https://pastebin.mozilla.org/8847465

Run result:

C:\...\>npm run migrate
> publish.webmaker.org@0.0.1 migrate C:\...\publish.webmaker.org
> knex migrate:latest

Using environment: development
Knex:warning - migrations failed with error: alter table "projects" add column "readonly" boolean, add column "client" text - relation "projects" does not exist
error: relation "projects" does not exist
    at Connection.parseE (C:\...\publish.webmaker.org\node_modules\pg\lib\connection.js:539:11)
    at Connection.parseMessage (C:\...\publish.webmaker.org\node_modules\pg\lib\connection.js:366:17)
    at Socket.<anonymous> (C:\...\publish.webmaker.org\node_modules\pg\lib\connection.js:105:22)
    at Socket.emit (events.js:107:17)
    at readableAddChunk (_stream_readable.js:163:16)
    at Socket.Readable.push (_stream_readable.js:126:10)
    at TCP.onread (net.js:538:20)
From previous event:
    at Client._query (C:\...\publish.webmaker.org\node_modules\knex\lib\dialects\postgres\index.js:122:12)
    at Client.query (C:\...\publish.webmaker.org\node_modules\knex\lib\client.js:127:24)
    at C:\...\publish.webmaker.org\node_modules\knex\lib\transaction.js:219:21
From previous event:
    at Client.trxClient.query (C:\...\publish.webmaker.org\node_modules\knex\lib\transaction.js:216:26)
    at Runner.<anonymous> (C:\...\publish.webmaker.org\node_modules\knex\lib\runner.js:118:24)
From previous event:
    at Runner.queryArray (C:\...\publish.webmaker.org\node_modules\knex\lib\runner.js:126:53)
    at C:\...\publish.webmaker.org\node_modules\knex\lib\runner.js:42:23
From previous event:
    at Runner.run (C:\...\publish.webmaker.org\node_modules\knex\lib\runner.js:30:20)
    at SchemaBuilder.Target.then (C:\...\publish.webmaker.org\node_modules\knex\lib\interface.js:27:43)
    at Object.exports.up (C:\...\publish.webmaker.org\migrations\2_alter_project_table.js:4:18)
    at C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:284:48
    at C:\...\publish.webmaker.org\node_modules\knex\lib\transaction.js:38:20
From previous event:
    at C:\...\publish.webmaker.org\node_modules\knex\lib\transaction.js:36:8
From previous event:
    at new Transaction (C:\...\publish.webmaker.org\node_modules\knex\lib\transaction.js:29:27)
    at Client.transaction (C:\...\publish.webmaker.org\node_modules\knex\lib\client.js:113:12)
    at Function.transaction (C:\...\publish.webmaker.org\node_modules\knex\lib\util\make-knex.js:39:21)
    at Migrator._transaction (C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:283:24)
    at Object.<anonymous> (C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:262:25)
From previous event:
    at C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:258:27
    at arrayEach (C:\...\publish.webmaker.org\node_modules\knex\node_modules\lodash\index.js:1289:13)
    at Function.<anonymous> (C:\...\publish.webmaker.org\node_modules\knex\node_modules\lodash\index.js:3345:13)
    at Migrator._waterfallBatch (C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:253:9)
    at C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:155:23
From previous event:
    at Migrator._runBatch (C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:154:10)
    at C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:44:22
    at processImmediate [as _immediateCallback] (timers.js:367:17)
From previous event:
    at Migrator.latest (C:\...\publish.webmaker.org\node_modules\knex\lib\migrate\index.js:43:63)
    at Command.<anonymous> (C:\...\publish.webmaker.org\node_modules\knex\lib\bin\cli.js:103:37)
    at Command.listener (C:\...\publish.webmaker.org\node_modules\knex\node_modules\commander\index.js:301:8)
    at Command.emit (events.js:110:17)
    at Command.parseArgs (C:\...\publish.webmaker.org\node_modules\knex\node_modules\commander\index.js:610:12)
    at Command.parse (C:\...\publish.webmaker.org\node_modules\knex\node_modules\commander\index.js:458:21)
    at Liftoff.invoke (C:\...\publish.webmaker.org\node_modules\knex\lib\bin\cli.js:142:13)
    at Liftoff.<anonymous> (C:\...\publish.webmaker.org\node_modules\knex\node_modules\liftoff\index.js:181:16)
    at module.exports (C:\...\publish.webmaker.org\node_modules\knex\node_modules\liftoff\node_modules\flagged-respawn\index.js:17:3)
    at Liftoff.<anonymous> (C:\...\publish.webmaker.org\node_modules\knex\node_modules\liftoff\index.js:174:9)
    at C:\...\publish.webmaker.org\node_modules\knex\node_modules\liftoff\index.js:148:9
    at C:\...\publish.webmaker.org\node_modules\knex\node_modules\v8flags\index.js:99:14
    at C:\...\publish.webmaker.org\node_modules\knex\node_modules\v8flags\index.js:38:7
    at process._tickCallback (node.js:355:11)
    at Function.Module.runMain (module.js:503:11)
    at startup (node.js:129:16)
    at node.js:814:3

There's also no mention which file things went wrong in, so that would normally make it harder to tell what knex is breaking on, except in this case there's only two migrations.

@ricardograca
Copy link
Member

It's probably not important, but Promise is already being passed as the second argument to both up and down, so you don't have to require it in your migration files. And on that note, how did you create the migration files?

Also, the affected file is right there in the stack trace:

at Object.exports.up (C:\...\publish.webmaker.org\migrations\2_alter_project_table.js:4:18)

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

missed that line. I am following the example from http://endpointsjs.com/tutorial/1 with your new recommendations on top. As for the promise being passed along, is that documented anywhere?

@ricardograca
Copy link
Member

What might be going on is that your database is in an inconsistent state, and the first migration was already recorded as completed, so it's not running again. I suggest you start over with a clean database and everything should work as intended. This is a weak spot for Knex's migrations, since they're not wrapped in a transaction, so if something goes wrong some things get applied and others not, which leaves the database in a messy state.

@ricardograca
Copy link
Member

Oh, and you should try the included docs first, which explain a more accurate process for creating migrations: http://knexjs.org/#Migrations-CLI

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

Oh no, trust me, I read those, and they're not actual documentation; go through the text you linked me too, and you'll notice there is no information about how to write a migration file or what the call signatures are, how they're called, when, and how to deal with midway errors.

There's some info on how to use the cli tool, that's about it. Heck, the text even talks about the props you can stick in a migration config object, then immediately moves on, never explaining any of those options.

Real docs are incredibly needed here, still. With the current documentation, migrations are a mystery subject, even though it's one of the most important aspects of a database orm =)

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

Cleaning wise: that's what the migrations table is for though? It should be telling knex which migrations already ran (in which direction and sequence). If we have no migration transactions, that's probably the absolute number one next bug to fix: a failed migration should never result in needing to wipe an entire database, as that has the potential to fatally break an already deployed system, simply because someone accidentally pushed code to master instead of to a staging branch, for instance.

@ricardograca
Copy link
Member

Currently, knex migrations aren't working as you expect. When something fails, the down migration won't run, and even if it did, it could try to undo things that don't exist, and that would also produce errors, so it's really just the lack of a transaction enveloping both the up and down methods that is missing for a much better user experience. There's an issue open for that.

You don't need to wipe the entire database when there is a failed migration. I was just proposing that for easiness, since you seem to just be starting with that project. You can manually revert the database state without losing any data if you prefer.

I know the docs are not very easy to follow, since I also struggled with this for a while, but all the information is there, just not clearly identified as such. It tells you how to create a a migration file, and by doing that you'll get a sample of how a a migration file looks like. Then you just have to use the appropriate schema builder methods.

I'm sure that if you would be willing to contribute improvements to the docs, they would get reviewed and merged quickly.

@rhys-vdw
Copy link
Member

Hi @Pomax. Sometimes the database gets into an inconsistent state when a migration is partially applied. By default (with the latest version) migrations will be run in transactions, preventing this issue - so unless you've knowingly disabled it you should be fine.

However it sounds as though you might be getting a race condition with Promise.all, or not returning the promise, allowing the migration to "complete" before the async code finishes executing.

exports.up = function(knex, Promise) {
  return knex.schema // make sure you're returning here.
  .createTable('publishedProjects', function(t) {
    t.increments('id');
    t.text('title').notNullable();
    t.text('tags');
    t.text('description');
  })
  .createTable('other table', function(t) {
}

Also turn on debug: true for more information on what knex is actually doing. Just set it in your knexfile with the other options.

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

Here's the problem: that's pretty much literally the code I was already using, for which @ricardograca told me to wrap things in Promise.all, but now you're suggesting that solution is actually a problem, so at this point I'm looking at this issue throwing my hands up in the air because it sounds like no one knows what the proper way to write a migration file is, and there are no docs that we can point to and say "just do this" (not just for me, but also and the many other people who tried this, had it fail, tried looking for docs, didn't find them, and just went back to Sequelize or the like).

Version wise, I'm using 0.8.6, which is the latest version as of this issue, as far as I can tell, and this always runs on an empty database for CI purposes. It's currently hooked into our PR testing setup, so updates to mozilla/publish.webmaker.org#166 based on advice in this issue end up getting built by travis in a clean environment, with the most recent Promise based migration yielding the following build failure: https://travis-ci.org/mozilla/publish.webmaker.org/builds/82093850

(https://travis-ci.org/mozilla/publish.webmaker.org/builds/82093850#L149 creates a brand new database every time CI runs, specifically to rule out problems relating to "bad old state")

@rhys-vdw
Copy link
Member

Hi @Pomax, I'm sorry to hear you're having a hard time. Please be aware that neither Ricardo nor I actually wrote Knex, nor this feature, we're essentially just users. I've already mentioned in another issue that I agree that there is a lack of documentation - sadly complaints alone will not ameliorate this situation. Unfortunately my contribution to knex is time limited to reviewing and merging community PRs and responding to occasional issues.

I can't see anything wrong with your linked code. I think Ricardo's suggestion of dropping all your tables and starting from scratch, coupled with turning on debug logs, should go a long way to helping you work out what's going on.

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

Yeah, sorry about my reactions. I've been trying to get unstuck on this for a while and have been venting the lack of progress as part of my unstuck process. Totally not okay, so apologies for that. The Travis instance always starts with an empty db, so I'm not sure how much more I can do there in terms of dropping tables, but I can try to put everything back to not using those promises and see where that gets me. Maybe there was something funny going on as rewrite-on-a-rewrite and that's been biting me.

@chrisbroome
Copy link

@Pomax @ricardograca From what I saw in the first migration file, using Promise.all won't work. This is because there are foreign keys in later tables that reference earlier tables. But those earlier tables might not have been created yet before a dependent migration has been run, so you'll get a foreign key error.

If you use bluebird's Promise.each, you should be ok because as the docs say,

Iteration happens serially

So, for example, your users table would be guaranteed to be created before trying to create your projects table.

@chrisbroome
Copy link

And @Pomax, for what's it worth, I always put only one operation in each migration file. In other words if i'm creating a bunch of tables, I'll create one migration for each table. The same goes for alters and views. Sure you end up with more migration files, but having more files should also (in theory - see my comment on individual migrations #666) give you more granular control on rollbacks.

@ricardograca
Copy link
Member

@chrisbroome Yes, if you put all of them inside the same .all() it won't work, but I usually chain several of those as needed to take care of dependencies. @Pomax Sorry if I missed that detail before, but I wasn't looking that deeply into your migrations to realize that could be a problem. @chrisbroome's advice is pretty good as well.

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

cool, let me try each, and I'll see about splitting up the migration to one file per table

@Pomax
Copy link
Author

Pomax commented Sep 25, 2015

each did not end up working, but chaining then on a promise.resolve seems to have gotten the job done:

exports.up = function (knex) {
  return new Promise.resolve()
  .then(function() {
    return knex.schema.createTable('publishedProjects', function(t) {
     ...
    });
  })
  .then(function() {
    return knex.schema.createTable('users', function (t) {
      ...
    });
  })
  .then(function() {
    return knex.schema.createTable('projects', function (t) {
      ...
    });
  })
  .then(function() {
    ...
  });
};

followed by file 2 with:

exports.up = function (knex) {
  return new Promise.resolve()
  .then(function() {
    return knex.schema.table('projects', function (t) {
      t.boolean('readonly');
      t.text('client');
    });
  });
};

leads to the initial creation and then succesful upward migration to extended schema.

Thanks for the help, and again apologies for being so rude!

@ricardograca
Copy link
Member

Using new Promise.resolve() to start a promise chain is a bit weird and totally unnecessary.

Here's how to do it in one file and making sure dependencies are taken care of:

exports.up = function(knex, Promise) {
    return Promise.all([
        knex.schema.createTable('blah', function(table) {
            table.increments()
            // doesn't depend on anything
        }),
        knex.schema.createTable('foo', function(table) {
            table.increments()
           // doesn't depend on anything
        })
    ]).then(function() {
        return Promise.all([
            knex.schema.createTable('bar', function(table) {
                table.increments()
                // this depends on something from the first Promise block above
            }),
            knex.schema.createTable('baz', function(table) {
                table.increments()
                // this depends on something from the first Promise block above
            })
        ])
    })
}

PS: You should read bluebird's documentation a few more times.

@Pomax
Copy link
Author

Pomax commented Sep 27, 2015

cheers

@Wtower
Copy link

Wtower commented Jan 3, 2016

This is a great thread. I am sorry for the unfortunate experiments of @Pomax . It seems that much information herein should be included into the official documentation about migrations. Is it possible to contribute to the official site's documentation?

Regarding the migration order, maybe we could use Django 1.7 excellent paradigm. A schema definition, which would allow in future the automatic creation of migration files. Migration files could feature something like dependencies in order to avoid the somewhat not-so-elegant way of filename ordering.

@jlmakes
Copy link

jlmakes commented Feb 7, 2016

Cheers @ricardograca that really helped 🙇

@elhigu
Copy link
Member

elhigu commented Feb 7, 2016

Looks like @Pomax did get his answer... closing the issue. Thanks everyone!

@elhigu elhigu closed this as completed Feb 7, 2016
@VirtualWolf
Copy link

@ricardograca That was INCREDIBLY helpful, thank you! An example like that in the documentation would be perfect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants