When running batch of knex
migrations, either through the API or via the CLI, the migrations might fail if they use ObjectionJS models. This can happen particularly in the case where the knexfile itself is resolved as an asynchronous function.
Setup
To explain this better, here is an example:
database.js
// This file stores logic responsible for providing credentials.
async function getKnexfile() {
// Some asynchronous behaviour that returns valid configuration.
// A good use case for this can be retrieving a secret stored in AWS Secrets Manager
// and passing it to the connection string part of the config.
//
// For this example, let's assume the following is returned:
return {
client: 'pg',
connectionString: 'pg://user:password@host:5432/database'
};
}
module.exports = { getKnexfile };
knexfile.js
module.exports = require('./database').getKnexfile();
Now let's consider two migration files that will be ran concurrently.
001_build_schema.js
exports.up = async (knex) => {
await knex.schema.createTable('mytable', (table) => {
table.string('id').unique().notNullable().primary();
table.string('text', 45);
});
}
exports.down = async (knex) => {
await knex.schema.dropTable('mytable');
}
And in the second migration file, we begin by importing one of the models. I'm not providing the complete source for that model because ultimately, the way it is defined does not really matter for this example. The important part however, is that (in my case) this model was making use of several plugins, such as knexSnakeCaseMappers()
, which together with the fact that my configuration was fetched asynchronously required some creative coding. The partial source for that model will be defined at the end.
002_insert_data.js
const MyModel = require('./MyModel');
exports.up = async (knex) => {
await MyModel.query().insert({text: 'My Text'});
}
exports.down = async (knex) => {
// Do nothing, this part is irrelevant...
}
The problem
What does not work, is running the two migrations as a batch. This means that triggering the batch of migrations (i.e. via CLI), causes them to fail like so:
# We are currently at the base migration (i.e. migrations were not ran yet).
knex migrate:latest
The above will result in the following error:
migration file "002_insert_data.js" failed
migration failed with error: insert into "mytable" ("text") values ($1) returning "id" - relation "mytable" does not exist
DBError: insert into "mytable" ("text") values ($1) returning "id" - relation "mytable" does not exist
This seemed like the migrations were not being awaited (i.e. migration 002 was running before migration 001 has finished), but experimenting with it has shown that this was not the case. Or at least, the problem was not as simple as the migrations not running one after another, since using simple console.log
statements have shown that these files were in fact executed concurrently.
Moreover, running the migrations one by one (i.e. not in a batch) using script similar to the following would result in successful migrations and the data would be populated in the database appropriately:
knex migrate:up && knex migrate:up
Having made sure that the schema used was identical across the board (setting .withSchema('schema_name')
), I figured out that the issue must have been related to the migrations being run in transactions, but using flag disableTransactions: true
has proven to be a poor solution, since in case of a crash, the database would be left in an unknown state.
Here is the partial source for MyModel.js
const { Model, knexSnakeCaseMappers, snakeCaseMappers } = require('objection');
// The below line imports an async function that returns the connection string. This is
// needed as knex() expects the provided argument to be an object, and accepts async function
// only for the connection field (which is why previously defined getKnexfile cannot be used).
const getConnectionStringAsync = require('./database');
const db = knex({
client: 'pg',
connection: knexfile.getConnectionString,
...knexSnakeCaseMappers(),
});
Model.knex(db);
module.exports = class MyModel extends Model {
// The implementation of the model goes here...
// The table name of this model is set to `mytable`.
}