{"id":13493573,"url":"https://github.com/fly-apps/safe-ecto-migrations","last_synced_at":"2026-01-26T22:54:53.184Z","repository":{"id":43293572,"uuid":"400879774","full_name":"fly-apps/safe-ecto-migrations","owner":"fly-apps","description":"Guide to Safe Ecto Migrations","archived":false,"fork":false,"pushed_at":"2024-10-20T21:04:05.000Z","size":38,"stargazers_count":311,"open_issues_count":7,"forks_count":12,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-01-17T18:53:32.925Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fly-apps.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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":"2021-08-28T20:00:25.000Z","updated_at":"2025-01-04T15:27:09.000Z","dependencies_parsed_at":"2024-01-16T09:52:45.461Z","dependency_job_id":"eb420fa2-0b84-469f-b663-c8ee044f2f7a","html_url":"https://github.com/fly-apps/safe-ecto-migrations","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fly-apps%2Fsafe-ecto-migrations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fly-apps%2Fsafe-ecto-migrations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fly-apps%2Fsafe-ecto-migrations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fly-apps%2Fsafe-ecto-migrations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fly-apps","download_url":"https://codeload.github.com/fly-apps/safe-ecto-migrations/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242961745,"owners_count":20213315,"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":[],"created_at":"2024-07-31T19:01:16.733Z","updated_at":"2026-01-26T22:54:53.171Z","avatar_url":"https://github.com/fly-apps.png","language":null,"funding_links":[],"categories":["Others"],"sub_categories":[],"readme":"# Safe Ecto Migrations\n\nA non-exhaustive guide on common migration recipes and how to avoid trouble.\n\n- [Adding an index](#adding-an-index)\n- [Adding a reference or foreign key](#adding-a-reference-or-foreign-key)\n- [Adding a column with a default value](#adding-a-column-with-a-default-value)\n- [Changing a column's default value](#changing-a-columns-default-value)\n- [Changing the type of a column](#changing-the-type-of-a-column)\n- [Removing a column](#removing-a-column)\n- [Renaming a column](#renaming-a-column)\n- [Renaming a table](#renaming-a-table)\n- [Adding a check constraint](#adding-a-check-constraint)\n- [Setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)\n- [Adding a JSON column](#adding-a-json-column)\n- [Squashing migrations](#squashing-migrations)\n\nRead more about safe migration techniques:\n\n- [Migration locks in \"Anatomy of a Migration\"](./Anatomy.md)\n- [How to backfill data and change data in bulk (aka: DML)](./Backfilling.md)\n\n---\n\n## Adding an index\n\nCreating an index will [block writes](https://www.postgresql.org/docs/8.2/sql-createindex.html) to the table in Postgres.\n\nMySQL is concurrent by default since [5.6](https://downloads.mysql.com/docs/mysql-5.6-relnotes-en.pdf) unless using `SPATIAL` or `FULLTEXT` indexes, which then it [blocks reads and writes](https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-operations.html#online-ddl-index-syntax-notes).\n\n**BAD ❌**\n\n```elixir\ndef change do\n  create index(\"posts\", [:slug])\n\n  # This obtains a ShareLock on \"posts\" which will block writes to the table\nend\n```\n\n**GOOD ✅**\n\nWith Postgres, instead create the index concurrently which does not block writes.\nThere are two options:\n\n**Option 1**\n\n[Configure the Repo to use advisory locks](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-migration-options) for locking migrations while running. Advisory locks are application-controlled database-level locks, and EctoSQL since v3.9.0 provides an option to use this type of lock. This is the safest option as it avoids the trade-off in Option 2.\n\nDisable the DDL transaction in the migration to avoid a database transaction which is not compatible with `CONCURRENTLY` database operations.\n\n```elixir\n# in config/config.exs\nconfig MyApp.Repo, migration_lock: :pg_advisory_lock\n\n# in the migration\n@disable_ddl_transaction true\n\ndef change do\n  create index(\"posts\", [:slug], concurrently: true)\nend\n```\n\nIf you're using Phoenix and PhoenixEcto, you will likely appreciate disabling\nthe migration lock in the CheckRepoStatus plug during dev to avoid hitting and\nwaiting on the advisory lock with concurrent web processes. You can do this by\nadding `migration_lock: false` to the CheckRepoStatus plug in your\n`MyAppWeb.Endpoint`.\n\n**Option 2**\n\nDisable the DDL transaction and the migration lock for the migration. By default, EctoSQL with Postgres will run migrations with a DDL transaction and a migration lock which also (by default) uses another transaction. You must disable both of these database transactions to use `CONCURRENTLY`. However, disabling the migration lock will allow competing nodes to try to run the same migration at the same time (eg, in a multi-node Kubernetes environment that runs migrations before startup). Therefore, some nodes may 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(\"posts\", [:slug], concurrently: true)\nend\n```\n\nFor either option chosen, the 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 a reference or foreign key\n\nAdding a foreign key blocks writes on both tables.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  alter table(\"posts\") do\n    add :group_id, references(\"groups\")\n    # Obtains a ShareRowExclusiveLock which blocks writes on both tables\n  end\nend\n```\n\n\n**GOOD ✅**\n\nIn the first migration\n\n```elixir\ndef change do\n  alter table(\"posts\") do\n    add :group_id, references(\"groups\", validate: false)\n    # Obtains a ShareRowExclusiveLock which blocks writes on both tables.\n  end\nend\n```\n\nIn the second migration\n\n```elixir\ndef change do\n  execute \"ALTER TABLE posts VALIDATE CONSTRAINT group_id_fkey\", \"\"\n  # Obtains a ShareUpdateExclusiveLock which doesn't block reads or writes\nend\n```\n\nThese migrations can be in the same deployment, but make sure they are separate migrations.\n\n**Note on empty tables**: when the table creating the referenced column is empty, you may be able to\ncreate the column and validate at the same time since the time difference would be milliseconds\nwhich may not be noticeable, no matter if you have 1 million or 100 million records in the referenced table.\n\n**Note on populated tables**: the biggest difference depends on your scale. For 1 million records in\nboth tables, you may lock writes to both tables when creating the column for milliseconds\n(you should benchmark for yourself) which could be acceptable for you. However, once your table has\n100+ million records, the difference becomes seconds which is more likely to be felt and cause timeouts.\nThe differentiating metric is the time that both tables are locked from writes. Therefore, err on the side\nof safety and separate constraint validation from referenced column creation when there is any data in the table.\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. If the default column is an expression (volatile value) it will remain unsafe.\n\n**BAD ❌**\n\nNote: This becomes safe for non-volatile (static) defaults in:\n\n- [Postgres 11+](https://www.postgresql.org/docs/release/11.0/). Default applies to INSERT since 7.x, and UPDATE since 9.3.\n- MySQL 8.0.12+\n- MariaDB 10.3.2+\n\n```elixir\ndef change do\n  alter table(\"comments\") do\n    add :approved, :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```elixir\ndef change do\n  alter table(\"comments\") do\n    add :some_timestamp, :utc_datetime, default: fragment(\"now()\")\n    # A volatile value\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(\"comments\") do\n    add :approved, :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  execute \"ALTER TABLE comments ALTER COLUMN approved SET DEFAULT false\",\n          \"ALTER TABLE comments ALTER COLUMN approved DROP DEFAULT\"\n  # This took 0.28 milliseconds for 100 million rows with no fkeys,\nend\n```\n\nNote: we cannot use [`modify/3`](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#modify/3) as it will include updating the column type as\nwell unnecessarily, causing Postgres to rewrite the table. For more information,\n[see this example](https://github.com/fly-apps/safe-ecto-migrations/issues/10).\n\nSchema change to read the new column:\n\n```diff\nschema \"comments\" do\n+ field :approved, :boolean, default: false\nend\n```\n\n\u003e [!NOTE]\n\u003e The safe method will not materialize the default value on the column for existing rows because the default was not set when adding the column (avoiding a potential table lock so it can re-write it to _write_ the default). However, the next `UPDATE` operation on the row will materialize the default, additionally Ecto will apply the default on the application side when reading the record. If you want to materialize the value, then you will need to consider [backfilling](./Backfilling.md).\n\n---\n\n## Changing a column's default value\n\nChanging an existing column's default may risk rewriting the table.\n\n**BAD ❌**\n\n```elixir\ndef change do\n  alter table(\"comments\") do\n    # Previously, the default was `true`\n    modify :approved, :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\nThe issue is that we cannot use [`modify/3`](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#modify/3) as it will include updating the column type as\nwell unnecessarily, causing Postgres to rewrite the table. For more information,\n[see this example](https://github.com/fly-apps/safe-ecto-migrations/issues/10).\n\n**GOOD ✅**\n\nExecute raw sql instead to alter the default:\n\n```elixir\ndef change do\n  execute \"ALTER TABLE comments ALTER COLUMN approved SET DEFAULT false\",\n          \"ALTER TABLE comments ALTER COLUMN approved SET DEFAULT true\"\n  # This took 0.28 milliseconds for 100 million rows with no fkeys,\nend\n```\n\n\u003e [!NOTE]\n\u003e This will not update the values of rows previously-set by the old default. This value has been materialized at the time of insert/update and therefore has no distinction between whether it was set by the column `DEFAULT` or set by the original operation.\n\u003e\n\u003e If you want to update the default of already-written rows, you must distinguish them somehow and modify them with a [backfill](./Backfilling.md)\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(\"posts\") 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## 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(\"posts\") do\n    remove :no_longer_needed_column\n  end\nend\n```\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 MyApp.Post do\n  schema \"posts\" do\n-   column :no_longer_needed_column, :text\n  end\nend\n```\n\nSecond deployment:\n\n```elixir\ndef change do\n  alter table(\"posts\") do\n    remove :no_longer_needed_column\n  end\nend\n```\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 \"posts\" do\n  field :summary, :text\nend\n\n\n# In your migration\ndef change do\n  rename table(\"posts\"), :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 MyApp.MySchema do\n  use Ecto.Schema\n\n  schema \"weather\" do\n    field :temp_lo, :integer\n    field :temp_hi, :integer\n    field :precipitation, :float, source: :prcp\n    field :city, :string\n\n    timestamps(type: :naive_datetime_usec)\n  end\nend\n```\n\n```diff\n## Update references in other parts of the codebase:\n   my_schema = Repo.get(MySchema, \"my_id\")\n-  my_schema.prcp\n+  my_schema.precipitation\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(\"posts\"), to: table(\"articles\")\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 MyApp.Weather do\n+ defmodule MyApp.Forecast do\n  use Ecto.Schema\n\n  schema \"weather\" do\n    field :temp_lo, :integer\n    field :temp_hi, :integer\n    field :precipitation, :float, source: :prcp\n    field :city, :string\n\n    timestamps(type: :naive_datetime_usec)\n  end\nend\n\n# and in calling code:\n- weather = MyApp.Repo.get(MyApp.Weather, “my_id”)\n+ forecast = MyApp.Repo.get(MyApp.Forecast, “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(\"products\", :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(\"products\", :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 products 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(\"products\") do\n    modify :active, :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(\"products\", :active_not_null, check: \"active 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 products VALIDATE CONSTRAINT active_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**However** we cannot use [`modify/3`](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#modify/3)\nas it will include updating the column type as well unnecessarily, causing\nPostgres to rewrite the table. For more information, [see this example](https://github.com/fly-apps/safe-ecto-migrations/issues/10).\n\n```elixir\n# **Postgres 12+ only**\n\ndef change do\n  execute \"ALTER TABLE products VALIDATE CONSTRAINT active_not_null\",\n          \"\"\n\n  execute \"ALTER TABLE products ALTER COLUMN active SET NOT NULL\",\n          \"ALTER TABLE products ALTER COLUMN active DROP NOT NULL\"\n\n  drop constraint(\"products\", :active_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## 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(\"posts\") do\n    add :extra_data, :json\n  end\nend\n```\n\n**GOOD ✅**\n\nUse jsonb instead. Some say it’s like “json” but “better.”\n\n```elixir\ndef change do\n  alter table(\"posts\") do\n    add :extra_data, :jsonb\n  end\nend\n```\n\n---\n\n## Squashing Migrations\n\nIf you have a long list of migrations, sometimes it can take a while to migrate\neach of those files every time the project is reset or spun up by a new\ndeveloper. Thankfully, Ecto comes with mix tasks to `dump` and `load` a database\nstructure which will represent the state of the database up to a certain point\nin time, not including content.\n\n- [mix ecto.dump](mix_ecto_dump)\n- [mix ecto.load](mix_ecto_load)\n\nSchema dumping and loading is only supported by external binaries `pg_dump` and\n`mysqldump`, which are used by the Postgres, MyXQL, and MySQL Ecto adapters (not\nsupported in MSSQL adapter).\n\nFor example:\n\n```\n20210101000000 - First Migration\n20210201000000 - Second Migration\n20210701000000 - Third Migration \u003c-- we are here now. run `mix ecto.dump`\n```\n\nWe can \"squash\" the migrations up to the current day which will effectively\nfast-forward migrations to that structure. The Ecto Migrator will detect that\nthe database is already migrated to the third migration, and so it begins there\nand migrates forward.\n\nLet's add a new migration:\n\n```\n20210101000000 - First Migration\n20210201000000 - Second Migration\n20210701000000 - Third Migration \u003c-- `structure.sql` represents up to here\n20210801000000 - New Migration \u003c-- This is where migrations will begin\n```\n\nThe new migration will still run, but the first-through-third migrations will\nnot need to be run since the structure already represents the changes applied by\nthose migrations. At this point, you can safely delete the first, second, and\nthird migration files or keep them for historical auditing.\n\nLet's make this work:\n\n1. Run `mix ecto.dump` which will dump the current structure into\n   `priv/repo/structure.sql` by default. [Check the mix task for more\n   options](mix_ecto_dump).\n2. During project setup with an empty database, run `mix ecto.load` to load\n   `structure.sql`.\n3. Run `mix ecto.migrate` to run any additional migrations created after the\n   structure was dumped.\n\nTo simplify these actions into one command, we can leverage mix aliases:\n\n```elixir\n# mix.exs\n\ndefp aliases do\n  [\n    \"ecto.reset\": [\"ecto.drop\", \"ecto.setup\"],\n    \"ecto.setup\": [\"ecto.load\", \"ecto.migrate\"],\n    # ...\n  ]\nend\n```\n\nNow you can run `mix ecto.setup` and it will load the database structure and run\nremaining migrations. Or, run `mix ecto.reset` and it will drop and run setup.\nOf course, you can continue running `mix ecto.migrate` as you create them.\n\n[mix_ecto_dump]: https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Dump.html\n[mix_ecto_load]: https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Load.html\n\n---\n\n# Credits\n\nCreated and written by David Bernheisel with recipes heavily inspired from Andrew Kane and his library [strong_migrations](https://github.com/ankane/strong_migrations).\n\n[PostgreSQL at Scale by James Coleman](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)\n\n[Strong Migrations by Andrew Kane](https://github.com/ankane/strong_migrations)\n\n[Adding a NOT NULL CONSTRAINT on PG Faster with Minimal Locking by Christophe Escobar](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c)\n\n[Postgres Runtime Configuration](https://www.postgresql.org/docs/current/runtime-config-client.html)\n\n[Automatic and Manual Ecto Migrations by Wojtek Mach](https://dashbit.co/blog/automatic-and-manual-ecto-migrations)\n\nSpecial thanks for sponsorship:\n\n* Fly.io\n\nSpecial thanks for these reviewers:\n\n* Steve Bussey\n* Stephane Robino\n* Dennis Beatty\n* Wojtek Mach\n* Mark Ericksen\n* And [all the contributors](https://github.com/fly-apps/safe-ecto-migrations/graphs/contributors)\n\n# Reference Material\n\n[Postgres Lock Conflicts](https://www.postgresql.org/docs/12/explicit-locking.html)\n\n|  |  **Current Lock →** | | | | | | | |\n|---------------------|-------------------|-|-|-|-|-|-|-|\n| **Requested Lock ↓** | ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE |\n| ACCESS SHARE           |   |   |   |   |   |   |   | X |\n| ROW SHARE              |   |   |   |   |   |   | X | X |\n| ROW EXCLUSIVE          |   |   |   |   | X | X | X | X |\n| SHARE UPDATE EXCLUSIVE |   |   |   | X | X | X | X | X |\n| SHARE                  |   |   | X | X |   | X | X | X |\n| SHARE ROW EXCLUSIVE    |   |   | X | X | X | X | X | X |\n| EXCLUSIVE              |   | X | X | X | X | X | X | X |\n| ACCESS EXCLUSIVE       | X | X | X | X | X | X | X | X |\n\n- `SELECT` acquires a `ACCESS SHARE` lock\n- `SELECT FOR UPDATE` acquires a `ROW SHARE` lock\n- `UPDATE`, `DELETE`, and `INSERT` will acquire a `ROW EXCLUSIVE` lock\n- `CREATE INDEX CONCURRENTLY` and `VALIDATE CONSTRAINT` acquires `SHARE UPDATE EXCLUSIVE`\n- `CREATE INDEX` acquires `SHARE` lock\n\nKnowing this, let's re-think the above table:\n\n|  |  **Current Operation →** | | | | | | | |\n|---------------------|-------------------|-|-|-|-|-|-|-|\n| **Blocks Operation ↓** | `SELECT` | `SELECT FOR UPDATE` | `UPDATE` `DELETE` `INSERT` | `CREATE INDEX CONCURRENTLY`  `VALIDATE CONSTRAINT` | `CREATE INDEX` | SHARE ROW EXCLUSIVE | EXCLUSIVE | `ALTER TABLE` `DROP TABLE` `TRUNCATE` `REINDEX` `CLUSTER` `VACUUM FULL` |\n| `SELECT` |   |   |   |   |   |   |   | X |\n| `SELECT FOR UPDATE` |   |   |   |   |   |   | X | X |\n| `UPDATE` `DELETE` `INSERT` |   |   |   |   | X | X | X | X |\n| `CREATE INDEX CONCURRENTLY` `VALIDATE CONSTRAINT` |   |   |   | X | X | X | X | X |\n| `CREATE INDEX` |   |   | X | X |   | X | X | X |\n| SHARE ROW EXCLUSIVE |   |   | X | X | X | X | X | X |\n| EXCLUSIVE |   | X | X | X | X | X | X | X |\n| `ALTER TABLE` `DROP TABLE` `TRUNCATE` `REINDEX` `CLUSTER` `VACUUM FULL` | X | X | X | X | X | X | X | X |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffly-apps%2Fsafe-ecto-migrations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffly-apps%2Fsafe-ecto-migrations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffly-apps%2Fsafe-ecto-migrations/lists"}