Where to start on a project for Ecto migration safety

I’m really interested in Elixir, and as a starter project wanted to put together a tool similar to theStrong Migrations gem for Rails. Briefly, this would examine your (pending) Ecto migrations and inform you if they could impact a running service (e.g. removing an existing column, the table lock required by non-concurrent index creation).

I’ve spent a bit of time reading up on the basics of Elixir, and had a quick look at Ecto, and I’m a bit stumped on how this could be accomplished.

The original strong_migrations gem simply overrides methods within ActiveRecord to decorate them with safety checks. I’ve never much liked this approach, but don’t see much of a choice when you’re trying to add behavior on top of a closed system.

It looks like the cleanest way this could be done would be to create another module that could be used after (or maybe instead of) Ecto.Migration. This would keep that same overriding behavior and warn users when it encounters a problematic migration. I see a lot of potential for problems here, mostly due to the same issues you always encounter when trying to override existing code. Also, before I got too deep to really understand what I was reading, it looked like I might run into problems with methods not being tagged as overrideable anyways.

Another idea I had was to set up a “proxy runner”. This would stand in between the migration and Ecto.Migration.Runner, acting as a kind of gatekeeper. My hope there would be to pick up on Ecto.Migration's internal calls to Runner methods and touch up the behavior of those. I’m relatively sure this won’t work since Ecto.Migration is already working against an alias of the Runner module, but I’m still a little hazy on when that would be resolved. (i.e. if I aliased something else to Runner within the migration, would Ecto.Migration methods pick up on that alias or keep using their own)

The last idea I had was to put together a custom adapter and catch things there. This has the problem of sometimes needing to “be” the real adapter (i.e. when querying out current migration state) but mostly not. So, again, it looks like trying to override an existing thing would be happening. On the plus side, that would make it easier to sort out any database-specific limitations.

Can anyone shine some light on these ideas? I know I’m probably diving pretty deep for a first experiment, but this struck my interest and seems like it could produce something valuable for the community.

1 Like

Rather than trying to override the expected behavior of Ecto I would keep it separate. You could have a SafeMigrate.analyze() function that analyzes the current database and migrations and returns either :safe or {:unsafe, reasons}. Then you can wrap it in a safe_migrate.analyze Mix task. You could create a watcher that watches for migration additions/changes and runs the analysis for you.

Then you could create another Mix task safe_migrate.migrate that utilizes the previous analysis and calls ecto.migrate if everything is safe:

# this might be a little bit more involved, but something like this
def migrate do
  case SafeMigrate.analyze() do
    :safe ->
      Mix.Task.run("ecto.migrate")
    {:unsafe, reasons} ->
      handle_unsafe(reasons)
  end
end

The analyzer module would be able to use any of the functionality provided by the public Ecto apis to obtain the information it needs to do its analysis. I think of of note would be Ecto.Migrator.migrated_versions and Ecto.Migrator.migrations, though I’ll admit that I’m not too familiar with the Ecto apis.

You might be able alias ecto.migrate to safe_migrate.migrate in mix.exs in order to “force” migrations to be ran safely, but I’m not sure if Mix aliases take precedence over existing commands.

2 Likes