{"id":13879477,"url":"https://github.com/LendingHome/zero_downtime_migrations","last_synced_at":"2025-07-16T15:32:28.182Z","repository":{"id":48660899,"uuid":"71187912","full_name":"LendingHome/zero_downtime_migrations","owner":"LendingHome","description":"Zero downtime migrations with ActiveRecord 3+ and PostgreSQL","archived":true,"fork":false,"pushed_at":"2023-08-24T12:43:34.000Z","size":64,"stargazers_count":562,"open_issues_count":17,"forks_count":27,"subscribers_count":90,"default_branch":"master","last_synced_at":"2024-11-16T02:46:47.681Z","etag":null,"topics":["activerecord","database","migrations","postgres","rails","ruby"],"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/LendingHome.png","metadata":{"files":{"readme":"README.md","changelog":null,"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}},"created_at":"2016-10-17T22:53:20.000Z","updated_at":"2024-11-04T03:40:29.000Z","dependencies_parsed_at":"2024-01-13T20:57:18.414Z","dependency_job_id":"8d536ce3-2964-4cd3-8c9c-d507c562a067","html_url":"https://github.com/LendingHome/zero_downtime_migrations","commit_stats":{"total_commits":54,"total_committers":6,"mean_commits":9.0,"dds":0.09259259259259256,"last_synced_commit":"8a16b59f5a3422aa84522d3b7c9866f3b73939d0"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LendingHome%2Fzero_downtime_migrations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LendingHome%2Fzero_downtime_migrations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LendingHome%2Fzero_downtime_migrations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LendingHome%2Fzero_downtime_migrations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LendingHome","download_url":"https://codeload.github.com/LendingHome/zero_downtime_migrations/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226143895,"owners_count":17580245,"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":["activerecord","database","migrations","postgres","rails","ruby"],"created_at":"2024-08-06T08:02:22.286Z","updated_at":"2024-11-24T08:31:23.456Z","avatar_url":"https://github.com/LendingHome.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# ![LendingHome](https://avatars0.githubusercontent.com/u/5448482?s=24\u0026v=4) zero_downtime_migrations\n[![Code Climate](https://codeclimate.com/github/LendingHome/zero_downtime_migrations/badges/gpa.svg)](https://codeclimate.com/github/LendingHome/zero_downtime_migrations) [![Coverage](https://codeclimate.com/github/LendingHome/zero_downtime_migrations/badges/coverage.svg)](https://codeclimate.com/github/LendingHome/zero_downtime_migrations) [![Gem Version](https://badge.fury.io/rb/zero_downtime_migrations.svg)](http://badge.fury.io/rb/zero_downtime_migrations)\n\n\u003e Zero downtime migrations with ActiveRecord 3+ and PostgreSQL.\n\nCatch problematic migrations at development/test time! Heavily inspired by these similar projects:\n\n* https://github.com/ankane/strong_migrations\n* https://github.com/foobarfighter/safe-migrations\n\n## Installation\n\nSimply add this gem to the project `Gemfile`.\n\n```ruby\ngem \"zero_downtime_migrations\"\n```\n\n## Usage\n\nThis gem will automatically **raise exceptions when potential database locking migrations are detected**.\n\nIt checks for common things like:\n\n* Adding a column with a default\n* Adding a non-concurrent index\n* Mixing data changes with index or schema migrations\n* Performing data or schema migrations with the DDL transaction disabled\n* Using `each` instead of `find_each` to loop thru `ActiveRecord` objects\n\nThese exceptions display clear instructions of how to perform the same operation the \"zero downtime way\".\n\n## Validations\n\n### Adding a column with a default\n\n#### Bad\n\nThis can take a long time with significant database size or traffic and lock your table!\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  def change\n    add_column :posts, :published, :boolean, default: true\n  end\nend\n```\n\n#### Good\n\nFirst let’s add the column without a default. When we add a column with a default it has to lock the table while it performs an UPDATE for ALL rows to set this new default.\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  def change\n    add_column :posts, :published, :boolean\n  end\nend\n```\n\nThen we’ll set the new column default in a separate migration. Note that this does not update any existing data! This only sets the default for newly inserted rows going forward.\n\n```ruby\nclass SetPublishedDefaultOnPosts \u003c ActiveRecord::Migration\n  def change\n    change_column_default :posts, :published, from: nil, to: true\n  end\nend\n```\n\nFinally we’ll backport the default value for existing data in batches. This should be done in its own migration as well. Updating in batches allows us to lock 1000 rows at a time (or whatever batch size we prefer).\n\n```ruby\nclass BackportPublishedDefaultOnPosts \u003c ActiveRecord::Migration\n  def up\n    say_with_time \"Backport posts.published default\" do\n      Post.unscoped.select(:id).find_in_batches.with_index do |batch, index|\n        say(\"Processing batch #{index}\\r\", true)\n        Post.unscoped.where(id: batch).update_all(published: true)\n      end\n    end\n  end\nend\n```\n\n### Adding an index concurrently\n\n#### Bad\n\nThis action can lock your database table while indexing existing data!\n\n```ruby\nclass IndexUsersOnEmail \u003c ActiveRecord::Migration\n  def change\n    add_index :users, :email\n  end\nend\n```\n\n#### Good\n\nInstead, let's add the index concurrently in its own migration with the DDL transaction disabled.\n\nThis allows PostgreSQL to build the index without locking in a way that prevent concurrent inserts, updates, or deletes on the table. Standard indexes lock out writes (but not reads) on the table.\n\n```ruby\nclass IndexUsersOnEmail \u003c ActiveRecord::Migration\n  disable_ddl_transaction!\n\n  def change\n    add_index :users, :email, algorithm: :concurrently\n  end\nend\n```\n\n### Mixing data/index/schema migrations\n\n#### Bad\n\nPerforming migrations that change the schema, update data, or add indexes within one big transaction is unsafe!\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  def change\n    add_column :posts, :published, :boolean\n    Post.unscoped.update_all(published: true)\n    add_index :posts, :published\n  end\nend\n```\n\n#### Good\n\nInstead, let's split apart these types of migrations into separate files.\n\n* Introduce schema changes with methods like `create_table` or `add_column` in one file. These should be run within a DDL transaction so that they can be rolled back if there are any issues.\n* Update data with methods like `update_all` or `save` in another file. Data migrations tend to be much more error prone than changing the schema or adding indexes.\n* Add indexes concurrently within their own file as well. Indexes should be created without the DDL transaction enabled to avoid table locking.\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  def change\n    add_column :posts, :published, :boolean\n  end\nend\n```\n\n```ruby\nclass BackportPublishedOnPosts \u003c ActiveRecord::Migration\n  def up\n    Post.unscoped.update_all(published: true)\n  end\nend\n```\n\n```ruby\nclass IndexPublishedOnPosts \u003c ActiveRecord::Migration\n  disable_ddl_transaction!\n\n  def change\n    add_index :posts, :published, algorithm: :concurrently\n  end\nend\n```\n\n### Disabling the DDL transaction\n\n#### Bad\n\nThe DDL transaction should only be disabled for migrations that add indexes. All other types of migrations should keep the DDL transaction enabled so that changes can be rolled back if any unexpected errors occur.\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  disable_ddl_transaction!\n\n  def change\n    add_column :posts, :published, :boolean\n  end\nend\n```\n\n```ruby\nclass UpdatePublishedOnPosts \u003c ActiveRecord::Migration\n  disable_ddl_transaction!\n\n  def up\n    Post.unscoped.update_all(published: true)\n  end\nend\n```\n\n#### Good\n\nAny other data or schema changes must live in their own migration files with the DDL transaction enabled just in case they make changes that need to be rolled back.\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  def change\n    add_column :posts, :published, :boolean\n  end\nend\n```\n\n```ruby\nclass UpdatePublishedOnPosts \u003c ActiveRecord::Migration\n  def up\n    Post.unscoped.update_all(published: true)\n  end\nend\n```\n\n### Looping thru `ActiveRecord::Base` objects\n\n#### Bad\n\nThis might accidentally load tens or hundreds of thousands of records into memory all at the same time!\n\n```ruby\nclass BackportPublishedDefaultOnPosts \u003c ActiveRecord::Migration\n  def up\n    Post.unscoped.each do |post|\n      post.update_attribute(published: true)\n    end\n  end\nend\n```\n\n#### Good\n\nLet's use the `find_each` method to fetch records in batches instead.\n\n```ruby\nclass BackportPublishedDefaultOnPosts \u003c ActiveRecord::Migration\n  def up\n    Post.unscoped.find_each do |post|\n      post.update_attribute(published: true)\n    end\n  end\nend\n```\n\n### TODO\n\n* Changing a column type\n* Removing a column\n* Renaming a column\n* Renaming a table\n\n## Disabling \"zero downtime migration\" enforcements\n\nWe can disable any of these \"zero downtime migration\" enforcements by wrapping them in a `safety_assured` block.\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  def change\n    safety_assured do\n      add_column :posts, :published, :boolean, default: true\n    end\n  end\nend\n```\n\nWe can also mark an entire migration as safe by using the `safety_assured` helper method.\n\n```ruby\nclass AddPublishedToPosts \u003c ActiveRecord::Migration\n  safety_assured\n\n  def change\n    add_column :posts, :published, :boolean\n    Post.unscoped.where(\"created_at \u003e= ?\", 1.day.ago).update_all(published: true)\n  end\nend\n```\n\nEnforcements can be globally disabled by setting `ENV[\"SAFETY_ASSURED\"]` when running migrations.\n\n```bash\nSAFETY_ASSURED=1 bundle exec rake db:migrate --trace\n```\n\nThese enforcements are **automatically disabled by default for the following scenarios**:\n\n* The database schema is being loaded with `rake db:schema:load` instead of `db:migrate`\n* The current migration is a reverse (down) migration\n* The current migration is named `RollupMigrations`\n\n## Testing\n\n```bash\nbundle exec rspec\n```\n\n## Contributing\n\n* Fork the project.\n* Make your feature addition or bug fix.\n* Add tests for it. This is important so we don't break it in a future version unintentionally.\n* Commit, do not mess with the version or history.\n* Open a pull request. Bonus points for topic branches.\n\n## Authors\n\n* [Sean Huber](https://github.com/shuber)\n\n## License\n\n[MIT](https://github.com/lendinghome/zero_downtime_migrations/blob/master/LICENSE) - Copyright © 2016 LendingHome\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLendingHome%2Fzero_downtime_migrations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FLendingHome%2Fzero_downtime_migrations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FLendingHome%2Fzero_downtime_migrations/lists"}