Excellent Migrations
Detect potentially dangerous or destructive operations in your database migrations.
Installation
The package can be installed by adding excellent_migrations to your list of dependencies
in mix.exs:
def deps do
[
{:excellent_migrations, "~> 0.1.0"}
]
endHow It Works
This tool analyzes code (AST) of migration files. You don't have to edit or include anything in your
migration files, except for ocassionally adding module attribute @safety_assured.
How to use it
There are multiple ways to integrate with Excellent Migrations.
mix task
mix excellent_migrations.check_safety
This mix task analyzes migrations and logs a warning for each danger detected.
migration task
mix excellent_migrations.migrate
Running this task will first analyze migrations. If no dangers are detected it will proceed and
run mix ecto.migrate. If there are any, it will log errors and stop.
Credo check
Excellent Migrations provide custom check for Credo.
Add ExcellentMigrations.CredoCheck.MigrationsSafety to your .credo file. Example warnings:
Warnings - please take a look
┃
┃ [W] ↗ Raw SQL used
┃ apps/cookbook/priv/repo/migrations/20211024133700_create_recipes.exs:13 #(Cookbook.Repo.Migrations.CreateRecipes.up)
┃ [W] ↗ Index added not concurrently
┃ apps/cookbook/priv/repo/migrations/20211024133705_create_index_on_veggies.exs:37 #(Cookbook.Repo.Migrations.CreateIndexOnVeggies.up)Code
You can also use it in code. To do so, you need to get AST of your migration, e.g.
via Code.string_to_quoted/2 and
pass it to ExcellentMigrations.DangersDetector.detect_dangers(ast). It will return keyword list
containing danger types and lines where they were detected.
Checks
Potentially dangerous operations:
- Adding a check constraint
- Adding a column with a default value
- Backfilling data
- Changing the type of a column
- Executing SQL directly
- Removing a column
- Renaming a column
- Renaming a table
- Setting NOT NULL on an existing column
Postgres-specific checks:
Best practices:
You can also disable specific checks.
Removing a column
Example
defmodule Cookbook.RemoveSizeFromDumplings do
def change do
alter table(:dumplings) do
remove :size, :string
end
end
endAdding a column with a default value
Example
defmodule Cookbook.AddTasteToDumplingsWithDefault do
def change do
alter table(:dumplings) do
add(:taste, :string, default: "sweet")
end
end
endBackfilling data
Example
defmodule Cookbook.BackfillRecords do
def change do
Repo.insert!(%Dumpling{taste: "umami"})
end
endChanging the type of a column
Example
defmodule Cookbook.ChangeColumnSizeTypeToInteger do
def change do
alter table(:dumplings) do
modify(:size, :integer)
end
end
endRenaming a column
Example
defmodule Cookbook.RenameFillingToStuffing do
def change do
rename table(:dumplings), :filling, to: :stuffing
end
endRenaming a table
Example
defmodule Cookbook.RenameDumplingsToNoodles do
def change do
rename(table(:dumplings), to: table("noodles"))
end
endAdding a check constraint
Example
defmodule Cookbook.CreatePriceConstraint do
def change do
create constraint("dumplings", :price_must_be_positive, check: "price > 0")
end
endSetting NOT NULL on an existing column
Example
defmodule Cookbook.AddNotNullOnShape do
def change do
alter table(:dumplings) do
modify :shape, :integer, null: true
end
end
endExecuting SQL directly
Example
defmodule Cookbook.CreateIndexOnDumplings do
def up do
execute("CREATE INDEX dumplings_geog ON dumplings using GIST(Geography(geom));")
end
def down do
execute("DROP INDEX dumplings_geog;")
end
endAdding an index non-concurrently
Example
defmodule Cookbook.AddIndex do
def change do
create index(:dumplings, [:recipe_id, :flour_id])
end
endAdding a reference
Example
defmodule Cookbook.AddReferenceToIngredient do
def change do
alter table(:recipes) do
modify :ingredient_id, references(:ingredients)
end
end
end
Adding a json column
defmodule Cookbook.AddDetailsJson do
def change do
add :details, :json, default: "{}"
end
endKeeping non-unique indexes to three columns or less
defmodule Cookbook.AddIndexOnIngredients do
def change do
alter table(:dumplings) do
create index(:ingredients, [:a, :b, :c, :d], concurrently: true)
end
end
endAssuring safety
To mark an operation in a migration as safe list it in @safety_assured attribute. It will be
ignored during analysis.
defmodule Cookbook.AddTasteToDumplingsWithDefault do
@safety_assured [:column_added_with_default]
def change do
alter table(:dumplings) do
add(:taste, :string, default: "sweet")
end
create index(:dumplings, [:recipe_id, :flour_id])
end
end
You can also mark all operations as safe in a given migration by adding @safety_assured :all
defmodule Cookbook.BackfillRecords do
@safety_assured :all
def change do
Repo.insert!(%Dumpling{taste: "umami"})
end
endPossible operation types are:
:column_added_with_default:column_removed:column_renamed:column_type_changed:index_not_concurrently:many_columns_index:not_null_added:operation_delete:operation_insert:operation_update:raw_sql_executed:table_renamed
Disable checks
Ignore specific dangers for all migraion checks with:
config :excellent_migrations, skip_checks: [:raw_sql_executed, :not_null_added]Existing migrations
To skip analyzing migrations that were created before adding this package, set timestamp from the
last migration in start_after in config:
config :excellent_migrations, start_after: "20191026080101"Similar tools
Contributing
Everyone is encouraged to help improve this project. Here are a few ways you can help:
- Give feedback – your opinion matters
- Suggest or add new features, submit pull requests
- Report bugs
- Improve documentation