{"id":13878871,"url":"https://github.com/doctolib/safe-pg-migrations","last_synced_at":"2025-05-14T06:13:07.291Z","repository":{"id":41972486,"uuid":"152056318","full_name":"doctolib/safe-pg-migrations","owner":"doctolib","description":"Make your PostgreSQL migrations safe","archived":false,"fork":false,"pushed_at":"2025-04-28T13:28:28.000Z","size":371,"stargazers_count":504,"open_issues_count":11,"forks_count":23,"subscribers_count":106,"default_branch":"master","last_synced_at":"2025-04-28T14:33:51.803Z","etag":null,"topics":["criticality-tier0","dodo","downtime","github-actions","managed-by-terraform","postgres","rails-migrations"],"latest_commit_sha":null,"homepage":"","language":"Ruby","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/doctolib.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2018-10-08T09:50:45.000Z","updated_at":"2025-04-23T13:38:05.000Z","dependencies_parsed_at":"2024-04-12T09:27:39.174Z","dependency_job_id":"ee7c38c7-3082-43dc-a053-a3f0e0fa5180","html_url":"https://github.com/doctolib/safe-pg-migrations","commit_stats":{"total_commits":230,"total_committers":27,"mean_commits":8.518518518518519,"dds":0.6173913043478261,"last_synced_commit":"4328e18f52f9ef4923014900df4449c7ff65ef95"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctolib%2Fsafe-pg-migrations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctolib%2Fsafe-pg-migrations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctolib%2Fsafe-pg-migrations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doctolib%2Fsafe-pg-migrations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doctolib","download_url":"https://codeload.github.com/doctolib/safe-pg-migrations/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254081098,"owners_count":22011552,"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":["criticality-tier0","dodo","downtime","github-actions","managed-by-terraform","postgres","rails-migrations"],"created_at":"2024-08-06T08:02:02.774Z","updated_at":"2025-05-14T06:13:07.266Z","avatar_url":"https://github.com/doctolib.png","language":"Ruby","funding_links":[],"categories":["Ruby","ActiveRecord"],"sub_categories":[],"readme":"# safe-pg-migrations\n\nActiveRecord migrations for Postgres made safe.\n\n![safe-pg-migrations](./logo.png)\n\n## Requirements\n\n- Ruby 3.0+\n- Rails 6.1+\n- PostgreSQL 11.7+\n\n## Usage\n\nJust drop this line in your Gemfile:\n\n```rb\ngem 'safe-pg-migrations'\n```\n\n**Note: Do not run migrations via PgBouncer connection if it is configured to use transactional or statement pooling modes. You must run migrations via a direct Postgres connection, or configure PgBouncer to use session pooling mode.**\n\n## Example\n\nConsider the following migration:\n\n```rb\nclass AddPatientRefToAppointments \u003c ActiveRecord::Migration[6.0]\n  def change\n    add_reference :appointments, :patient\n  end\nend\n```\n\nIf the `users` table is large, running this migration will likely cause downtime. **Safe PG Migrations** hooks into Active Record so that the following gets executed instead:\n\n```rb\nclass AddPatientRefToAppointments \u003c ActiveRecord::Migration[6.0]\n  # Do not wrap the migration in a transaction so that locks are held for a shorter time.\n  disable_ddl_transaction!\n\n  def change\n    # Lower Postgres' lock timeout to avoid statement queueing. Acts like a seatbelt.\n    execute(\"SET lock_timeout TO '5s'\")\n\n    # Lower Postgres' statement timeout to avoid too long transactions. Acts like a seatbelt.\n    execute(\"SET statement_timeout TO '5s'\")\n    add_column :appointments, :patient_id, :bigint\n\n    # add_index using the concurrent algorithm, to avoid locking the tables\n    add_index :appointments, :patient_id, algorithm: :concurrently\n\n    # add_foreign_key without validation, to avoid locking the table for too long\n    execute(\"SET statement_timeout TO '5s'\")\n    add_foreign_key :appointments, :patients, validate: false\n\n    execute(\"SET statement_timeout TO '0'\")\n\n    # validate the foreign key separately, it avoids taking a lock on the entire tables\n    validate_foreign_key :appointments, :patients\n    \n    # we also need to set timeouts to their initial values if needed\n  end\nend\n```\n\nUnder the hood, **Safe PG Migrations** patches `ActiveRecord::Migration` and extends `ActiveRecord::Base.connection` to make potentially dangerous methods—like `add_reference`—safe.\n\n## Motivation\n\nWriting a safe migration can be daunting. Numerous articles, [including ours](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9), have been written on the topic and a few gems are trying to address the problem. Even for someone who has a pretty good command of Postgres, remembering all the subtleties of explicit locking is not a piece of cake.\n\nActive Record means developers don't have to be proficient in SQL to interact with a database. In the same way, **Safe PG Migrations** was created so that developers don't have to understand the ins and outs of Postgres to write a safe migration.\n\n## Feature set\n\n\u003cdetails\u003e\u003csummary\u003eLock timeout\u003c/summary\u003e\n\nMost DDL operations (e.g. adding a column, removing a column or adding a default value to a column) take an `ACCESS EXCLUSIVE` lock on the table they are altering. While these operations wait to acquire their lock, other statements are blocked. Before running a migration, **Safe PG Migrations** sets a short lock timeout (default to 5 seconds) so that statements are not blocked for too long.\n\nSee [PostgreSQL Alter Table and Long Transactions](http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html) and [Migrations and Long Transactions](https://www.fin.com/post/2018/1/migrations-and-long-transactions) for detailed explanations of the matter.\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStatement timeout\u003c/summary\u003e\n\nAdding a foreign key or a not-null constraint can take a lot of time on a large table. The problem is that those operations take `ACCESS EXCLUSIVE` locks. We clearly don't want them to hold these locks for too long. Thus, **Safe PG Migrations** runs them with a short statement timeout (default to 5 seconds).\n\nSee [Zero-downtime Postgres migrations - the hard parts](https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/) for a detailed explanation on the subject.\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003ePrevent wrapping migrations in transaction\u003c/summary\u003e\n\nWhen **Safe PG Migrations** is used, migrations are not wrapped in a transaction. This is for several reasons:\n\n- We want to release locks as soon as possible.\n- In order to be able to retry statements that have failed because of a lock timeout, we have to be outside a transaction.\n- In order to add an index concurrently, we have to be outside a transaction.\n\nNote that if a migration fails, it won't be rolled back. This can result in migrations being partially applied. In that case, they need to be manually reverted.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSafe \u003ccode\u003eadd_column\u003c/code\u003e\u003c/summary\u003e\n\n**Safe PG Migrations** gracefully handle the upgrade to PG11 by **not** backfilling default value for existing rows, as the [database engine is now natively handling it](https://www.postgresql.org/docs/11/ddl-alter.html#DDL-ALTER-ADDING-A-COLUMN).\n\nBeware though, when adding a volatile default value: \n```ruby\nadd_column :users, :created_at, default: 'clock_timestamp()'\n```\nPG will still needs to update every row of the table, and will most likely statement timeout for big table. In this case, **Safe PG Migrations** can automatically backfill data when the option `default_value_backfill:` is set to `:update_in_batches`. \n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSafe add_column - adding a volatile default value\u003c/summary\u003e\n\n**Safe PG Migrations** provides the extra option parameter `default_value_backfill:`. When your migration is adding a volatile default value, the option `:update_in_batches` can be set. It will automatically backfill the value in a safe manner.\n\n```ruby\nsafety_assured do\n  add_column :users, :created_at, default: 'clock_timestamp()', default_value_backfill: :update_in_batches\nend\n```\n\nMore specifically, it will: \n\n1. create the column without default value and without null constraint. This ensure the `ACCESS EXCLUSIVE` lock is acquired for the least amount of time;\n2. add the default value, without data backfill. An `ACCESS EXCLUSIVE` lock is acquired and released immediately;\n3. backfill data, in batch of `SafePgMigrations.config.backfill_batch_size` and with a pause of `SafePgMigrations.config.backfill_pause` between each batch;\n4. change the column to `null: false`, if defined in the parameters, following the algorithm we have defined below.\n\n---\n**NOTE**\n\nData backfill take time. If your table is big, your migrations will (safely) hangs for a while. You might want to backfill data manually instead, to do so you will need two migrations\n\n1. First migration :\n\n    a. adds the column without default and without null constraint;\n\n    b. add the default value.\n\n2. manual data backfill (rake task, manual operation, ...)\n3. Second migration which change the column to null false (with **Safe PG Migrations**, `change_column_null` is safe and can be used; see section below)\n---\n\n`default_value_backfill:` also accept the value `:auto` which is set by default. In this case, **Safe PG Migrations** will not backfill data and will let PostgreSQL handle it itself.\n\n### Preventing :update_in_batches when the table is too big\n\n`add_column` with `default_value_backfill: :update_in_batches` can be dangerous on big tables. To avoid unwanted long migrations, **Safe PG Migrations** does not automatically mark this usage as safe when used with `strong-migrations`, usage of `safety_assured` is required.\n\nIt is also possible to set a threshold for the table size, above which the migration will fail. This can be done by setting the `default_value_backfill_threshold:` option in the configuration.\n\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary id=\"safe_add_remove_index\"\u003eSafe \u003ccode\u003eadd_index\u003c/code\u003e and \u003ccode\u003eremove_index\u003c/code\u003e\u003c/summary\u003e\n\nCreating an index requires a `SHARE` lock on the target table which blocks all write on the table while the index is created (which can take some time on a large table). This is usually not practical in a live environment. Thus, **Safe PG Migrations** ensures indexes are created concurrently.\n\nAs `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` are non-blocking operations (ie: read/write operations on the table are still possible), **Safe PG Migrations** sets a lock timeout to 30 seconds for those 2 specific statements.\n\nIf you still get lock timeout while adding / removing indexes, it might be for one of those reasons:\n\n- Long-running queries are active on the table. To create / remove an index, PG needs to wait for the queries that are actually running to finish before starting the index creation / removal. The blocking activity logger might help you to pinpoint the culprit queries.\n- A vacuum / autovacuum is running on the table, holding a ShareUpdateExclusiveLock, you are most likely out of luck for the current migration, but you may try to [optimize your autovacuums settings](https://www.percona.com/blog/2018/08/10/tuning-autovacuum-in-postgresql-and-autovacuum-internals/).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary id=\"safe_add_foreign_key\"\u003eSafe \u003ccode\u003eadd_foreign_key\u003c/code\u003e (and \u003ccode\u003eadd_reference\u003c/code\u003e)\u003c/summary\u003e\n\nAdding a foreign key requires a `SHARE ROW EXCLUSIVE` lock, which **prevent writing in the tables** while the migration is running.\n\nAdding the constraint itself is rather fast, the major part of the time is spent on validating this constraint. Thus **Safe PG Migrations** ensures that adding a foreign key holds blocking locks for the least amount of time by splitting the foreign key creation in two steps: \n\n1. adding the constraint *without validation*, will not validate existing rows;\n2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table\n\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\u003csummary id=\"safe_add_check_constraint\"\u003eSafe \u003ccode\u003eadd_check_constraint\u003c/code\u003e (ActiveRecord \u003e 6.1)\u003c/summary\u003e\n\nAdding a check constraint requires an `ACCESS EXCLUSIVE` lock, which **prevent writing and reading in the tables** [as soon as the lock is requested](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9).\n\nAdding the constraint itself is rather fast, the major part of the time is spent on validating this constraint.\nThus **Safe PG Migrations** ensures that adding a constraints holds blocking locks for the least amount of time by\nsplitting the constraint addition in two steps: \n\n1. adding the constraint *without validation*, will not validate existing rows;\n2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary id=\"safe_change_column_null\"\u003eSafe \u003ccode\u003echange_column_null\u003c/code\u003e (ActiveRecord and PG version dependant)\u003c/summary\u003e\n\nChanging the nullability of a column requires an `ACCESS EXCLUSIVE` lock, which **prevent writing and reading in the tables** [as soon as the lock is requested](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9).\n\nAdding the constraint itself is rather fast, the major part of the time is spent on validating this constraint.\n\n**Safe PG Migrations** acts differently depending on the version you are on. \n\n### Recent versions of PG and Active Record (\u003e 12 and \u003e 6.1)\n\nStarting on PostgreSQL versions 12, adding the column NOT NULL constraint is safe if a check constraint validates the\nnullability of the same column. **Safe PG Migrations** also relies on add_check_constraint, which was introduced in\nActiveRecord 6.1.  \n\nIf these requirements are met, **Safe PG Migrations** ensures that adding a constraints holds blocking locks for the least\namount of time by splitting the constraint addition in several steps: \n\n1. adding a `IS NOT NULL` constraint *without validation*, will not validate existing rows but block read or write;\n2. validating the constraint, will validate existing rows in the table, without blocking read or write on the table;\n3. changing the not null status of the column, thanks to the NOT NULL constraint without having to scan the table sequentially;\n4. dropping the `IS NOT NULL` constraint.\n\n### Older versions of PG or ActiveRecord\n\nIf the version of PostgreSQL is below 12, or if the version of ActiveRecord is below 6.1, **Safe PG Migrations** will only\nwrap ActiveRecord method into a statement timeout and lock timeout.\n\n### Call with a default parameter\n\nCalling change_column_null with a default parameter [is dangerous](https://github.com/rails/rails/blob/716baea69f989b64f5bfeaff880c2512377bebab/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L446)\nand is likely not to finish in the statement timeout defined by **Safe PG Migrations**. For this reason, when the default\nparameter is given, **Safe PG Migrations** will simply forward it to activerecord methods without trying to improve it\n\n### Dropping a NULL constraint\n\nDropping a null constraint still requires an `ACCESS EXCLUSIVE` lock, but does not require extra operation to reduce the\namount of time during which the lock is held. **Safe PG Migrations** only wrap methods of activerecord in lock and statement\ntimeouts\n\n\u003c/details\u003e\n\n\n\n\u003cdetails\u003e\u003csummary\u003eRetry after lock timeout\u003c/summary\u003e\n\nWhen a statement fails with a lock timeout, **Safe PG Migrations** retries it (5 times max) [list of retriable statements](https://github.com/doctolib/safe-pg-migrations/blob/66933256252b6bbf12e404b829a361dbba30e684/lib/safe-pg-migrations/plugins/statement_retrier.rb#L5)\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eBlocking activity logging\u003c/summary\u003e\n\nIf a statement fails with a lock timeout, **Safe PG Migrations** will try to tell you what was the blocking statement.\n\n---\n**NOTE**\n\nData logged by the Blocking activity logger can be sensitive (it will contain raw SQL queries, which can be hashes of password, user information, ...)\n\nIf you cannot afford to log this type of data, you can either\n* Set `SafePgMigrations.config.blocking_activity_logger_verbose = false`. In this case, the logger will only log the pid of the blocking statement, which should be enough to investigate;\n* Provide a different logger through `SafePgMigrations.config.sensitive_logger = YourLogger.new`. Instead of using the default IO stream, SafePgMigrations will send sensitive data to the given logger which can be managed as you wish.\n\n---\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eDropping a table\u003c/summary\u003e\n\nDropping a table can be difficult to achieve in a small amount of time if it holds several foreign keys to busy tables.\nTo remove the table, PostgreSQL will have to acquire an access exclusive lock on all the tables referenced by the foreign keys.\n\nTo solve this issue, **Safe Pg Migrations** will drop the foreign keys before dropping the table.\n\n---\n**NOTE**\n\nDropping a table is a dangerous operation by nature. **Safe Pg Migrations** will not prevent the deletion of a table which\nwould still be in use.\n\n---\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eVerbose SQL logging\u003c/summary\u003e\n\nFor any operation, **Safe PG Migrations** can output the performed SQL queries. This feature is enabled by default in a production Rails environment. If you want to explicit enable it, for example in your development environment you can use:\n```bash\nexport SAFE_PG_MIGRATIONS_VERBOSE=1\n```\n\nInstead of the traditional output:\n```ruby\nadd_index :users, :age\n\n== 20191215132355 SampleIndex: migrating ======================================\n-- add_index(:users, :age)\n   -\u003e add_index(\"users\", :age, {:algorithm=\u003e:concurrently})\n   -\u003e 0.0175s\n== 20191215132355 SampleIndex: migrated (0.0200s) =============================\n```\n**Safe PG Migrations** will output the following logs:\n```ruby\nadd_index :users, :age\n\n== 20191215132355 SampleIndex: migrating ======================================\n   (0.3ms)  SHOW lock_timeout\n   (0.3ms)  SET lock_timeout TO '5s'\n-- add_index(:users, :age)\n   -\u003e add_index(\"users\", :age, {:algorithm=\u003e:concurrently})\n   (0.3ms)  SHOW statement_timeout\n   (0.3ms)  SET statement_timeout TO 0\n   (0.3ms)  SHOW lock_timeout\n   (0.3ms)  SET lock_timeout TO '30s'\n   (3.5ms)  CREATE INDEX CONCURRENTLY \"index_users_on_age\" ON \"users\"  (\"age\")\n   (0.3ms)  SET lock_timeout TO '5s'\n   (0.2ms)  SET statement_timeout TO '1min'\n   -\u003e 0.0093s\n   (0.2ms)  SET lock_timeout TO '0'\n== 20191215132355 SampleIndex: migrated (0.0114s) =============================\n```\nSo you can actually check that the `CREATE INDEX` statement will be performed concurrently, without any statement timeout and with a lock timeout of 30 seconds.\n\n*Nb: The `SHOW` statements are used by **Safe PG Migrations** to query settings for their original values in order to restore them after the work is done*\n\n\u003c/details\u003e\n\n## Configuration\n\n**Safe PG Migrations** can be customized, here is an example of a Rails initializer (the values are the default ones):\n\n```ruby\nSafePgMigrations.config.safe_timeout = 5.seconds # Statement timeout used for all DDL operations except from CREATE / DROP INDEX\n\nSafePgMigrations.config.lock_timeout = nil # Lock timeout used for all DDL operations except from CREATE / DROP INDEX. If not set, safe_timeout will be used with a deduction of 1% to ensure that the lock timeout is raised in priority\n\nSafePgMigrations.config.increase_lock_timeout_on_retry # Activate the lock timeout increase feature on retry if set to true. See max_lock_timeout_for_retry for more information.\n\nSafePgMigrations.config.max_lock_timeout_for_retry = 1.second # Max lock timeout for the retries for all DDL operations except from CREATE / DROP INDEX. Each retry will increase the lock_timeout (if increase_lock_timeout_on_retry option is set to true) by (max_lock_timeout_for_retry - lock_timeout) / max_tries\n\nSafePgMigrations.config.blocking_activity_logger_verbose = true # Outputs the raw blocking queries on timeout. When false, outputs information about the lock instead\n\nSafePgMigrations.config.sensitive_logger = nil # When given, sensitive data will be sent to this logger instead of the standard output. Must implement method `info`.\n\nSafePgMigrations.config.blocking_activity_logger_margin = 1.second # Delay to output blocking queries before timeout. Must be shorter than safe_timeout\n\nSafePgMigrations.config.backfill_batch_size = 100_000 # Size of the batches used for backfilling when adding a column with a default value\n\nSafePgMigrations.config.backfill_pause = 0.5.second # Delay between each batch during a backfill. This ensure replication can happen safely. \n\nSafePgMigrations.config.default_value_backfill_threshold = nil # When set, batch backfill will only be available if the table is under the given threshold. If the number of rows is higher (according to stats), the migration will fail. \n\nSafePgMigrations.config.retry_delay = 1.minute # Delay between retries for retryable statements\n\nSafePgMigrations.config.max_tries = 5 # Number of retries before abortion of the migration\n```\n\n## Authors\n\n- [Matthieu Prat](https://github.com/matthieuprat)\n- [Romain Choquet](https://github.com/rchoquet)\n- [Thomas Hareau](https://github.com/ThHareau)\n- [Paul-Etienne Coisne](https://github.com/coisnepe)\n\n## License\n\n[MIT](https://github.com/doctolib/safe-pg-migrations/blob/master/LICENSE) © [Doctolib](https://github.com/doctolib/)\n\n## Additional resources\n\nAlternatives:\n\n- https://github.com/gocardless/activerecord-safer_migrations\n- https://github.com/ankane/strong_migrations\n- https://github.com/LendingHome/zero_downtime_migrations\n\nInteresting reads:\n\n- [When Postgres blocks: 7 tips for dealing with locks](https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/)\n- [Migrations and Long Transactions in Postgres\n](https://www.fin.com/post/2018/1/migrations-and-long-transactions)\n- [PostgreSQL Alter Table and Long Transactions](http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html)\n- [Adding a NOT NULL CONSTRAINT on PG Faster with Minimal Locking](https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c)\n- [Adding columns with default values to really large tables in Postgres + Rails](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/)\n- [Rails migrations with no downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)\n- [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)\n- [Rails Migrations with Zero Downtime](https://blog.codeship.com/rails-migrations-zero-downtime/)\n- [Stop worrying about PostgreSQL locks in your Rails migrations](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9)\n- [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoctolib%2Fsafe-pg-migrations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoctolib%2Fsafe-pg-migrations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoctolib%2Fsafe-pg-migrations/lists"}