{"id":16415428,"url":"https://github.com/artur-sulej/excellent_migrations","last_synced_at":"2025-04-08T12:08:27.306Z","repository":{"id":45396339,"uuid":"205240438","full_name":"Artur-Sulej/excellent_migrations","owner":"Artur-Sulej","description":"An Elixir tool for checking safety of database migrations.","archived":false,"fork":false,"pushed_at":"2024-08-10T00:37:36.000Z","size":104,"stargazers_count":254,"open_issues_count":21,"forks_count":31,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-01T10:17:16.136Z","etag":null,"topics":["ast","code-analysis","credo","ecto","elixir","migrations","static-analysis"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Artur-Sulej.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-29T19:55:46.000Z","updated_at":"2025-03-17T01:41:59.000Z","dependencies_parsed_at":"2024-01-05T21:54:14.690Z","dependency_job_id":"be029748-df23-4813-8827-8c828d26047c","html_url":"https://github.com/Artur-Sulej/excellent_migrations","commit_stats":{"total_commits":129,"total_committers":10,"mean_commits":12.9,"dds":"0.37209302325581395","last_synced_commit":"b205594c3e3e6b5a948b90ed4d14b18c2a1937ed"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Artur-Sulej%2Fexcellent_migrations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Artur-Sulej%2Fexcellent_migrations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Artur-Sulej%2Fexcellent_migrations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Artur-Sulej%2Fexcellent_migrations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Artur-Sulej","download_url":"https://codeload.github.com/Artur-Sulej/excellent_migrations/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247838444,"owners_count":21004580,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","code-analysis","credo","ecto","elixir","migrations","static-analysis"],"created_at":"2024-10-11T07:05:41.194Z","updated_at":"2025-04-08T12:08:27.283Z","avatar_url":"https://github.com/Artur-Sulej.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Excellent Migrations\n\n[![CI Tests](https://github.com/artur-sulej/excellent_migrations/workflows/Tests/badge.svg)](https://github.com/artur-sulej/excellent_migrations/actions?query=branch%3Amaster)\n[![Module Version](https://img.shields.io/hexpm/v/excellent_migrations.svg)](https://hex.pm/packages/excellent_migrations)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/excellent_migrations/)\n[![Total Download](https://img.shields.io/hexpm/dt/excellent_migrations.svg)](https://hex.pm/packages/excellent_migrations)\n[![License](https://img.shields.io/hexpm/l/excellent_migrations.svg)](https://github.com/artur-sulej/excellent_migrations/blob/master/LICENSE.md)\n[![Last Updated](https://img.shields.io/github/last-commit/artur-sulej/excellent_migrations.svg)](https://github.com/artur-sulej/excellent_migrations/commits/master)\n\nDetect potentially dangerous or destructive operations in your database migrations.\n\n## Installation\n\nThe package can be installed by adding `:excellent_migrations` to your list of dependencies\nin `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:excellent_migrations, \"~\u003e 0.1\", only: [:dev, :test], runtime: false}\n  ]\nend\n```\n\n## Documentation\n\nDocumentation is available on [Hexdocs](https://hexdocs.pm/excellent_migrations/).\n\n## How It Works\n\nThis tool analyzes code (AST) of migration files. You don't have to edit or include any additional\ncode in your migration files, except for occasionally adding a config comment\nfor [assuring safety](#assuring-safety).\n\n## How to use it\n\nThere are multiple ways to integrate with Excellent Migrations.\n\n### Credo check\n\nExcellent Migrations provide custom, ready-to-use check for [Credo](https://github.com/rrrene/credo).\n\nAdd `ExcellentMigrations.CredoCheck.MigrationsSafety` to your `.credo` file:\n\n```elixir\n%{\n  configs: [\n    %{\n      # …\n      checks: [\n        # …\n        {ExcellentMigrations.CredoCheck.MigrationsSafety, []}\n      ]\n    }\n  ]\n}\n\n```\n\nExample credo warnings:\n\n```\n  Warnings - please take a look\n┃\n┃ [W] ↗ Raw SQL used\n┃       apps/cookbook/priv/repo/migrations/20211024133700_create_recipes.exs:13 #(Cookbook.Repo.Migrations.CreateRecipes.up)\n┃ [W] ↗ Index added not concurrently\n┃       apps/cookbook/priv/repo/migrations/20211024133705_create_index_on_veggies.exs:37 #(Cookbook.Repo.Migrations.CreateIndexOnVeggies.up)\n```\n\n### mix task\n\n`mix excellent_migrations.check_safety`\n\nThis mix task analyzes migrations and logs a warning for each danger detected.\n\n### migration task\n\n`mix excellent_migrations.migrate`\n\nRunning this task will first analyze migrations. If no dangers are detected it will proceed and\nrun `mix ecto.migrate`. If there are any, it will log errors and stop.\n\n### Code\n\nYou can also use it in code. To do so, you need to get source code and AST of your migration file,\ne.g. via `File.read!/1`\nand [`Code.string_to_quoted/2`](https://hexdocs.pm/elixir/1.12/Code.html#string_to_quoted/2). Then\npass them to `ExcellentMigrations.DangersDetector.detect_dangers(ast)`. It will return a keyword\nlist containing danger types and lines where they were detected.\n\n## Checks\n\nPotentially dangerous operations:\n\n- [Adding a check constraint](#adding-a-check-constraint)\n- [Adding a column with a default value](#adding-a-column-with-a-default-value)\n- [Backfilling data](#backfilling-data)\n- [Column with volatile default](#column-with-volatile-default)\n- [Changing the type of a column](#changing-the-type-of-a-column)\n- [Executing SQL directly](#executing-SQL-directly)\n- [Removing a column](#removing-a-column)\n- [Renaming a column](#renaming-a-column)\n- [Renaming a table](#renaming-a-table)\n- [Setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)\n\nPostgres-specific checks:\n\n- [Adding a json column](#adding-a-json-column)\n- [Adding a reference](#adding-a-reference)\n- [Adding an index non-concurrently](#adding-an-index-non-concurrently)\n- [Adding an index concurrently without disabling lock or transaction](#adding-an-index-concurrently-without-disabling-lock-or-transaction)\n\nBest practices:\n\n- [Keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)\n\nYou can also [disable specific checks](#disable-checks).\n\n### Removing a column\n\nIf Ecto is still configured to read a column in any running instances of the application, then queries will fail when loading data into your structs. This can happen in multi-node deployments or if you start the application before running migrations.\n\n**BAD ❌**\n\n```elixir\n# Without a code change to the Ecto Schema\n\ndef change do\n  alter table(\"recipes\") do\n    remove :no_longer_needed_column\n  end\nend\n```\n\n**GOOD ✅**\n\nSafety can be assured if the application code is first updated to remove references to the column so it's no longer loaded or queried. Then, the column can safely be removed from the table.\n\n1. Deploy code change to remove references to the field.\n1. Deploy migration change to remove the column.\n\nFirst deployment:\n\n```diff\n# First deploy, in the Ecto schema\n\ndefmodule Cookbook.Recipe do\n  schema \"recipes\" do\n-   column :no_longer_needed_column, :text\n  end\nend\n```\n\nSecond deployment:\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    remove :no_longer_needed_column\n  end\nend\n```\n\n---\n\n### Adding a column with a default value\n\nAdding a column with a default value to an existing table may cause the table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.\n\n**BAD ❌**\n\nNote: This becomes safe in:\n\n- Postgres 11+\n- MySQL 8.0.12+\n- MariaDB 10.3.2+\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    add :favourite, :boolean, default: false\n    # This took 10 minutes for 100 million rows with no fkeys,\n\n    # Obtained an AccessExclusiveLock on the table, which blocks reads and\n    # writes.\n  end\nend\n```\n\n**GOOD ✅**\n\nAdd the column first, then alter it to include the default.\n\nFirst migration:\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    add :favourite, :boolean\n    # This took 0.27 milliseconds for 100 million rows with no fkeys,\n  end\nend\n```\n\nSecond migration:\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    modify :favourite, :boolean, default: false\n    # This took 0.28 milliseconds for 100 million rows with no fkeys,\n  end\nend\n```\n\nSchema change to read the new column:\n\n```diff\nschema \"recipes\" do\n+ field :favourite, :boolean, default: false\nend\n```\n\n---\n\n### Column with volatile default\n\nIf the default value is volatile (e.g., `clock_timestamp()`, `uuid_generate_v4()`, `random()`) each row will need to be updated with the value calculated at the time `ALTER TABLE` is executed.\n\n**BAD ❌**\n\nAdding volatile default to column:\n\n```elixir\ndef change do\n  alter table(:recipes) do\n    modify(:identifier, :uuid, default: fragment(\"uuid_generate_v4()\"))\n  end\nend\n```\n\nAdding column with volatile default:\n\n```elixir\ndef change do\n  alter table(:recipes) do\n    add(:identifier, :uuid, default: fragment(\"uuid_generate_v4()\"))\n  end\nend\n```\n\n**GOOD ✅**\n\nTo avoid a potentially lengthy update operation, particularly if you intend to fill the column with mostly nondefault values anyway, it may be preferable to:\n1. add the column with no default\n1. insert the correct values using `UPDATE` query\n1. only then add any desired default\n\nAlso creating a new table with column with volatile default is safe, because it does not contain any records. \n\n---\n\n### Backfilling data\n\nEcto creates a transaction around each migration, and backfilling in the same transaction that alters a table keeps the table locked for the duration of the backfill.\nAlso, running a single query to update data can cause issues for large tables.\n\n**BAD ❌**\n\n\n```elixir\ndefmodule Cookbook.BackfillRecipes do\n  use Ecto.Migration\n  import Ecto.Query\n\n  def change do\n    alter table(\"recipes\") do\n      add :new_data, :text\n    end\n\n    flush()\n\n    Cookbook.Recipe\n    |\u003e where(new_data: nil)\n    |\u003e Cookbook.Repo.update_all(set: [new_data: \"some data\"])\n  end\nend\n\n```\n\n**GOOD ✅**\n\nThere are several different strategies to perform safe backfilling. [This article](https://fly.io/phoenix-files/backfilling-data) explains them in great details.\n\n---\n\n### Changing the type of a column\n\nChanging the type of a column may cause the table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.\n\n**BAD ❌**\n\nSafe in Postgres:\n\n- increasing length on varchar or removing the limit\n- changing varchar to text\n- changing text to varchar with no length limit\n- Postgres 9.2+ - increasing precision (NOTE: not scale) of decimal or numeric columns. eg, increasing 8,2 to 10,2 is safe. Increasing 8,2 to 8,4 is not safe.\n- Postgres 9.2+ - changing decimal or numeric to be unconstrained\n- Postgres 12+ - changing timestamp to timestamptz when session TZ is UTC\n\nSafe in MySQL/MariaDB:\n\n- increasing length of varchar from \u003c 255 up to 255.\n- increasing length of varchar from \u003e 255 up to max.\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    modify :my_column, :boolean, from: :text\n  end\nend\n```\n\n**GOOD ✅**\n\nTake a phased approach:\n\n1. Create a new column\n1. In application code, write to both columns\n1. Backfill data from old column to new column\n1. In application code, move reads from old column to the new column\n1. In application code, remove old column from Ecto schemas.\n1. Drop the old column.\n\n---\n\n### Renaming a column\n\nAsk yourself: \"Do I _really_ need to rename a column?\". Probably not, but if you must, read on and be aware it requires time and effort.\n\nIf Ecto is configured to read a column in any running instances of the application, then queries will fail when loading data into your structs. This can happen in multi-node deployments or if you start the application before running migrations.\n\nThere is a shortcut: Don't rename the database column, and instead rename the schema's field name and configure it to point to the database column.\n\n**BAD ❌**\n\n```elixir\n# In your schema\nschema \"recipes\" do\n  field :summary, :text\nend\n\n\n# In your migration\ndef change do\n  rename table(\"recipes\"), :title, to: :summary\nend\n```\n\nThe time between your migration running and your application getting the new code may encounter trouble.\n\n**GOOD ✅**\n\n**Strategy 1**\n\nRename the field in the schema only, and configure it to point to the database column and keep the database column the same. Ensure all calling code relying on the old field name is also updated to reference the new field name.\n\n```elixir\ndefmodule Cookbook.Recipe do\n  use Ecto.Schema\n\n  schema \"recipes\" do\n    field :author, :string\n    field :preparation_minutes, :integer, source: :prep_min\n  end\nend\n```\n\n```diff\n## Update references in other parts of the codebase:\n   recipe = Repo.get(Recipe, \"my_id\")\n-  recipe.prep_min\n+  recipe.preparation_minutes\n```\n\n**Strategy 2**\n\nTake a phased approach:\n\n1. Create a new column\n1. In application code, write to both columns\n1. Backfill data from old column to new column\n1. In application code, move reads from old column to the new column\n1. In application code, remove old column from Ecto schemas.\n1. Drop the old column.\n\n---\n\n### Renaming a table\n\nAsk yourself: \"Do I _really_ need to rename a table?\". Probably not, but if you must, read on and be aware it requires time and effort.\n\nIf Ecto is still configured to read a table in any running instances of the application, then queries will fail when loading data into your structs. This can happen in multi-node deployments or if you start the application before running migrations.\n\nThere is a shortcut: rename the schema only, and do not change the underlying database table name.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  rename table(\"recipes\"), to: table(\"dish_algorithms\")\nend\n```\n\n**GOOD ✅**\n\n**Strategy 1**\n\nRename the schema only and all calling code, and don’t rename the table:\n\n```diff\n- defmodule Cookbook.Recipe do\n+ defmodule Cookbook.DishAlgorithm do\n  use Ecto.Schema\n\n  schema \"dish_algorithms\" do\n    field :author, :string\n    field :preparation_minutes, :integer\n  end\nend\n\n# and in calling code:\n- recipe = Cookbook.Repo.get(Cookbook.Recipe, \"my_id\")\n+ dish_algorithm = Cookbook.Repo.get(Cookbook.DishAlgorithm, \"my_id\")\n```\n\n**Strategy 2**\n\nTake a phased approach:\n\n1. Create the new table. This should include creating new constraints (checks and foreign keys) that mimic behavior of the old table.\n1. In application code, write to both tables, continuing to read from the old table.\n1. Backfill data from old table to new table\n1. In application code, move reads from old table to the new table\n1. In application code, remove the old table from Ecto schemas.\n1. Drop the old table.\n\n---\n\n### Adding a check constraint\n\nAdding a check constraint blocks reads and writes to the table in Postgres, and blocks writes in MySQL/MariaDB while every row is checked.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  create constraint(\"ingredients\", :price_must_be_positive, check: \"price \u003e 0\")\n  # Creating the constraint with validate: true (the default when unspecified)\n  # will perform a full table scan and acquires a lock preventing updates\nend\n```\n\n**GOOD ✅**\n\nThere are two operations occurring:\n\n1. Creating a new constraint for new or updating records\n1. Validating the new constraint for existing records\n\nIf these commands are happening at the same time, it obtains a lock on the table as it validates the entire table and fully scans the table. To avoid this full table scan, we can separate the operations.\n\nIn one migration:\n\n```elixir\ndef change do\n  create constraint(\"ingredients\", :price_must_be_positive, check: \"price \u003e 0\", validate: false)\n  # Setting validate: false will prevent a full table scan, and therefore\n  # commits immediately.\nend\n```\n\nIn the next migration:\n\n```elixir\ndef change do\n  execute \"ALTER TABLE ingredients VALIDATE CONSTRAINT price_must_be_positive\", \"\"\n  # Acquires SHARE UPDATE EXCLUSIVE lock, which allows updates to continue\nend\n```\n\nThese can be in the same deployment, but ensure there are 2 separate migrations.\n\n---\n\n### Setting NOT NULL on an existing column\n\nSetting NOT NULL on an existing column blocks reads and writes while every row is checked.  Just like the Adding a check constraint scenario, there are two operations occurring:\n\n1. Creating a new constraint for new or updating records\n1. Validating the new constraint for existing records\n\nTo avoid the full table scan, we can separate these two operations.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    modify :favourite, :boolean, null: false\n  end\nend\n```\n\n**GOOD ✅**\n\nAdd a check constraint without validating it, backfill data to satiate the constraint and then validate it. This will be functionally equivalent.\n\nIn the first migration:\n\n```elixir\n# Deployment 1\ndef change do\n  create constraint(\"recipes\", :favourite_not_null, check: \"favourite IS NOT NULL\", validate: false)\nend\n```\n\nThis will enforce the constraint in all new rows, but not care about existing rows until that row is updated.\n\nYou'll likely need a data migration at this point to ensure that the constraint is satisfied.\n\nThen, in the next deployment's migration, we'll enforce the constraint on all rows:\n\n```elixir\n# Deployment 2\ndef change do\n  execute \"ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null\", \"\"\nend\n```\n\nIf you're using Postgres 12+, you can add the NOT NULL to the column after validating the constraint. From the Postgres 12 docs:\n\n\u003e SET NOT NULL may only be applied to a column provided\n\u003e none of the records in the table contain a NULL value\n\u003e for the column. Ordinarily this is checked during the\n\u003e ALTER TABLE by scanning the entire table; however, if\n\u003e a valid CHECK constraint is found which proves no NULL\n\u003e can exist, then the table scan is skipped.\n\n```elixir\n# **Postgres 12+ only**\n\ndef change do\n  execute \"ALTER TABLE recipes VALIDATE CONSTRAINT favourite_not_null\", \"\"\n\n  alter table(\"recipes\") do\n    modify :favourite, :boolean, null: false\n  end\n\n  drop constraint(\"recipes\", :favourite_not_null)\nend\n```\n\nIf your constraint fails, then you should consider backfilling data first to cover the gaps in your desired data integrity, then revisit validating the constraint.\n\n---\n\n### Executing SQL directly\n\nExcellent Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:\n\n```elixir\ndefmodule Cookbook.ExecuteRawSql do\n  # excellent_migrations:safety-assured-for-this-file raw_sql_executed\n\n  def change do\n    execute(\"...\")\n  end\nend\n```\n\n---\n\n### Adding an index non-concurrently\n\nCreating an index will block both reads and writes.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  create index(\"recipes\", [:slug])\n\n  # This obtains a ShareLock on \"recipes\" which will block writes to the table\nend\n```\n\n**GOOD ✅**\n\nWith Postgres, instead create the index concurrently which does not block reads. You will need to disable the database transactions to use `CONCURRENTLY`, and since Ecto obtains migration locks through database transactions this also implies that competing nodes may attempt to try to run the same migration (eg, in a multi-node Kubernetes environment that runs migrations before startup). Therefore, some nodes will fail startup for a variety of reasons. \n\n```elixir\n@disable_ddl_transaction true\n@disable_migration_lock true\n\ndef change do\n  create index(\"recipes\", [:slug], concurrently: true)\nend\n```\n\nThe migration may still take a while to run, but reads and updates to rows will continue to work. For example, for 100,000,000 rows it took 165 seconds to add run the migration, but SELECTS and UPDATES could occur while it was running.\n\n**Do not have other changes in the same migration**; only create the index concurrently and separate other changes to later migrations.\n\n---\n\n### Adding an index concurrently without disabling lock or transaction\n\nConcurrently indexes need to set both `@disable_ddl_transaction` and `@disable_migration_lock` to true. [See more](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#index/3-adding-dropping-indexes-concurrently):\n\n**BAD ❌**\n\n```elixir\ndefmodule Cookbook.AddIndex do\n  def change do\n    create index(:recipes, [:cookbook_id, :cuisine], concurrently: true)\n  end\nend\n```\n\n**GOOD ✅**\n\n```elixir\ndefmodule Cookbook.AddIndex do\n  @disable_ddl_transaction true\n  @disable_migration_lock true\n\n  def change do\n    create index(:recipes, [:cookbook_id, :cuisine], concurrently: true)\n  end\nend\n```\n\n---\n\n### Adding a reference\n\nAdding a foreign key blocks writes on both tables.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    add :cookbook_id, references(\"cookbooks\")\n  end\nend\n```\n\n**GOOD ✅**\n\nIn the first migration\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    add :cookbook_id, references(\"cookbooks\", validate: false)\n  end\nend\n```\n\nIn the second migration\n\n```elixir\ndef change do\n  execute \"ALTER TABLE recipes VALIDATE CONSTRAINT cookbook_id_fkey\", \"\"\nend\n```\n\n These migrations can be in the same deployment, but make sure they are separate migrations.\n\n---\n\n### Adding a `json` column\n\nIn Postgres, there is no equality operator for the json column type, which can cause errors for existing SELECT DISTINCT queries in your application.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    add :extra_data, :json\n  end\nend\n```\n\n**GOOD ✅**\n\nUse jsonb instead. Some say it’s like \"json\" but \"**b**etter.\"\n\n```elixir\ndef change do\n  alter table(\"recipes\") do\n    add :extra_data, :jsonb\n  end\nend\n```\n\n---\n\n### Keeping non-unique indexes to three columns or less\n\n**BAD ❌**\n\nAdding a non-unique index with more than three columns rarely improves performance.\n\n```elixir\ndefmodule Cookbook.AddIndexOnIngredients do\n  def change do\n    create index(:recipes, [:a, :b, :c, :d], concurrently: true)\n  end\nend\n```\n\n**GOOD ✅**\n\nInstead, start an index with columns that narrow down the results the most.\n\n```elixir\ndefmodule Cookbook.AddIndexOnIngredients do\n  def change do\n    create index(:recipes, [:b, :d], concurrently: true)\n  end\nend\n```\n\nFor Postgres, be sure to add them concurrently.\n\n---\n\n## Assuring safety\n\nTo mark an operation in a migration as safe use config comment. It will be ignored during analysis.\n\nThere are two config comments available:\n\n* `excellent_migrations:safety-assured-for-next-line \u003coperation_type\u003e`\n* `excellent_migrations:safety-assured-for-this-file \u003coperation_type\u003e`\n\nIgnoring checks for given line:\n\n```elixir\ndefmodule Cookbook.AddTypeToRecipesWithDefault do\n  def change do\n    alter table(:recipes) do\n      # excellent_migrations:safety-assured-for-next-line column_added_with_default\n      add(:type, :string, default: \"dessert\")\n    end\n  end\nend\n```\n\nIgnoring checks for the whole file:\n\n```elixir\ndefmodule Cookbook.AddTypeToRecipesWithDefault do\n  # excellent_migrations:safety-assured-for-this-file column_added_with_default\n\n  def change do\n    alter table(:recipes) do\n      add(:type, :string, default: \"dessert\")\n    end\n  end\nend\n```\n\nPossible operation types are:\n\n* `check_constraint_added`\n* `column_added_with_default`\n* `column_reference_added`\n* `column_removed`\n* `column_renamed`\n* `column_type_changed`\n* `column_volatile_default`\n* `index_concurrently_without_disable_ddl_transaction`\n* `index_concurrently_without_disable_migration_lock`\n* `index_not_concurrently`\n* `json_column_added`\n* `many_columns_index`\n* `not_null_added`\n* `operation_delete`\n* `operation_insert`\n* `operation_update`\n* `raw_sql_executed`\n* `table_dropped`\n* `table_renamed`\n\n## Disable checks\n\nIgnore specific dangers for all migration checks with:\n\n```elixir\nconfig :excellent_migrations, skip_checks: [:raw_sql_executed, :not_null_added]\n```\n\n## Existing migrations\n\nTo skip analyzing migrations that were created before adding this package, set timestamp from the\nlast migration in `start_after` in config:\n\n```elixir\nconfig :excellent_migrations, start_after: \"20191026080101\"\n```\n\n## Similar tools \u0026 resources\n\n* https://github.com/ankane/strong_migrations (Ruby)\n* https://github.com/rrrene/credo (Elixir)\n* https://github.com/fly-apps/safe-ecto-migrations – Special thanks for unsafe actions explanation and recipes.\n* https://www.postgresql.org/docs/current/sql-altertable.html#Notes\n\n## Contributing\n\nEveryone is encouraged and welcome to help improve this project. Here are a few ways you can help:\n\n- Give feedback – your opinion matters\n- Visit [TODO list](https://github.com/Artur-Sulej/excellent_migrations/projects/1)\n- [Submit pull request](https://github.com/Artur-Sulej/excellent_migrations/pulls)\n- [Suggest feature](https://github.com/Artur-Sulej/excellent_migrations/issues)\n- [Report bug](https://github.com/Artur-Sulej/excellent_migrations/issues)\n- Improve documentation\n\n## Copyright and License\n\nCopyright (c) 2021 Artur Sulej\n\nThis work is free. You can redistribute it and/or modify it under the terms of the MIT License. See\nthe [LICENSE.md](./LICENSE.md) file for more details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fartur-sulej%2Fexcellent_migrations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fartur-sulej%2Fexcellent_migrations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fartur-sulej%2Fexcellent_migrations/lists"}