{"id":29127043,"url":"https://github.com/andyatkinson/anchor_migrations","last_synced_at":"2026-01-20T17:30:16.161Z","repository":{"id":300686216,"uuid":"1006259504","full_name":"andyatkinson/anchor_migrations","owner":"andyatkinson","description":"⚓ Ship safe, idempotent Postgres DDL changes faster, stay in sync w/ Active Record Migrations.","archived":false,"fork":false,"pushed_at":"2025-07-12T03:11:43.000Z","size":89,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-09T19:54:35.893Z","etag":null,"topics":[],"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/andyatkinson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","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,"zenodo":null}},"created_at":"2025-06-21T21:13:14.000Z","updated_at":"2025-07-13T10:02:32.000Z","dependencies_parsed_at":"2025-06-23T04:40:55.341Z","dependency_job_id":null,"html_url":"https://github.com/andyatkinson/anchor_migrations","commit_stats":null,"previous_names":["andyatkinson/anchor_migrations"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/andyatkinson/anchor_migrations","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyatkinson%2Fanchor_migrations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyatkinson%2Fanchor_migrations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyatkinson%2Fanchor_migrations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyatkinson%2Fanchor_migrations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andyatkinson","download_url":"https://codeload.github.com/andyatkinson/anchor_migrations/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyatkinson%2Fanchor_migrations/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271517843,"owners_count":24773775,"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","status":"online","status_checked_at":"2025-08-21T02:00:08.990Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":"2025-06-30T00:08:40.573Z","updated_at":"2026-01-20T17:30:16.099Z","avatar_url":"https://github.com/andyatkinson.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⚓ Anchor Migrations\nAnchor Migrations are SQL DDL changes, safety-linted (non-blocking), idempotent, intended to augment and speed up DDL changes limited by ORM migrations.\n\nAnchor Migrations generate Active Record ORM migrations. They're both specific files, and a \"workflow.\"\n\n## Commands\n```sh\nanchor init         # initialize directories\nanchor generate     # generate empty versioned .sql file, to be filled in\nanchor lint         # safety-lint .sql files using Squawk\nanchor backfill     # Create Active Record migration from SQL\nanchor migrate      # Apply the Anchor Migration DDL\n```\n\n## Installation\nAdd `anchor_migrations` to your `development` and `test` Gemfile groups:\n```rb\ngroup :development, :test do\n  gem 'anchor_migrations'\nend\n```\nThen run `bundle install`.\n\n## Preconditions\nAnchor Migrations have a restricted workflow for now (may get more flexible) and expect a few things:\n- Postgres only, 13+ (minimum for `DROP INDEX CONCURRENTLY`)\n- `DATABASE_URL` environment variable set to target database (e.g. production), and is reachable\n- `psql` client accessible in PATH\n- [Squawk](https://squawkhq.com) executable ([Quick Start documentation](https://squawkhq.com/docs/)) accessible in PATH\n\n## Safety linting and lock_timeout\nSquawk is used on SQL migration files to check for unsafe operations. For example, creating or dropping an index without the `CONCURRENTLY` keyword is unsafe, and is detected by Squawk.\n\nAnchor Migrations require safety-linted SQL, although right now it's up to the developer to run `anchor lint` in their workflow and make it clear they did in their PR.\n\nAnchor Migration SQL is what's applied by running `anchor migrate`, and this requires a psql client. By default a 2 second `lock_timeout`[^docs] is set.\n\n## What problems do Anchor migrations solve?\nAnchor Migrations are a process for organizations not using Trunk Based Development[^tbd] or that have infrequent releases. Instead of being limited to Active Record Migrations,\nAnchor Migrations are used to perform DDL without code dependencies, more frequently, then Rails Migrations are used to keep the schema state in sync.\n\n## Example Anchor Migration SQL\nAnchor Migrations are stored in `db/anchor_migrations` as `.sql` files. For example:\n```sql\n-- db/anchor_migrations/20250623173850_create_index_trips_created_at.sql\nCREATE INDEX CONCURRENTLY IF NOT EXISTS\nidx_trips_created_at ON trips (created_at);\n```\n\nAlthough `anchor generate` will create a generic name, it's important to customize the file name since it will be used.\n\nAbove, the timestamp portion `20250623173850` was left as-generated, but `create_index_trips_created_at` was used to reflect the DDL and add some info about what's changing.\n\nSquawk runs on SQL files in the directory to perform \"safety linting,\" looking for unsafe patterns.\n\nRun it using the `lint` command:\n```sh\n➜ anchor lint\n\nFound 0 issues in 1 file 🎉\n```\n\nIf Squawk finds issues, apply the fixes and run it again until there are none.\n\n## Example Active Record ORM Migration\nThis Rails migration was generated from the Anchor Migration SQL above.\n\nStrong Migrations[^strong] is used in the parent Rails app, so the `safety_assured` block was added to the Rails migration `change` method.\n\nThis is a configurable option.\n```rb\n#\n# ################################################\n# DO NOT EDIT, generated by Anchor Migrations\n# Version: 20250623173850\n# Source File: db/anchor_migrations/20250623173850_create_index_trips_created_at.sql\n# Target File: db/migrate/20250623173850_create_index_trips_created_at.rb\n# ################################################\n#\nclass CreateIndexTripsCreatedAt \u003c ActiveRecord::Migration[7.2]\n  disable_ddl_transaction!\n\n  def change\n    safety_assured do\n      execute \u003c\u003c-SQL\n        CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_trips_created_at ON trips (created_at);\n      SQL\n    end\n  end\nend\n```\n\n## Configuration\nAnchor Migrations supports generating Strong Migrations-compatible Active Record migrations:\n```rb\n# config/initializers/anchor_migrations.rb\nAnchorMigrations.configure do |config|\n  config.use_strong_migrations = true\nend\n```\n\n## Example PR Workflow\nFor a PR, add:\n1. Generate the Anchor Migration SQL file using `anchor generate`. It's important to customize the name since it will be used in the Rails Migration class name and file name! Fill in the SQL DDL. Make sure it's idempotent.\n1. Lint the SQL file using `anchor lint`\n1. Backfill the Active Record Migration by running `anchor backfill`. You will need to re-format the Ruby code (`rubocop -a`) as it may be correct, but poorly formatted.\n1. Apply the Rails migration like normal: `rails db:migrate`. Include the migration and the changes to `db/structure.sql` or `db/schema.rb` in your PR.\n1. You're ready for review. Get an \"approval\" describing your plan to apply the Anchor Migration. The approval can be a comment in the PR from a team member.\n1. With an approval, run `anchor migrate`. Capture the output for the PR. Verify the changes were applied. Once verified, move the SQL migration into `db/anchor_migrations/applied` and update your PR.\n1. With that, you're all done. Get a PR approval and merge it in.\n\nThe Rails migration \"backfills\" the DDL for any database where it hasn't already applied. That will be local, CI, etc. but in production it won't apply since it already exists.\n\n## Good uses of Anchor Migrations\n### Query support, data integrity, data quality\nIndexes and constraints that support query performance or data integrity, don't have code dependencies, can now be added, removed and replaced at a faster cadence, while keeping everything consistent.\n\nIndexes and constraints improve performance and data quality, and arguably shouldn’t be \"blocked\" by a slow release process that constrains DDL changes.\n\n### Long running DDL changes\nOn large tables, creating indexes concurrently can take a long time. It's nice to perform that during a low activity period, requiring control over the exact timing, possibly retries, monitoring.\n\nThis isn't ideal for ORM migrations and deploys. However, Anchor Migrations are a good fit for this.\n\n## Anchor Migrations Properties\n### Idempotent\nAnchor Migrations in SQL must be written using idempotent tactics like `IF NOT EXISTS` or by checking that a constraint exists already or doesn't.\n\nThis allows the SQL to be the \"source\" for an Active Record migration, making it idempotent.\n\n### Restricted DDL: What DDL is supported for Anchor Migrations?\nOnly non-blocking, idempotent DDL is supported. This list is restricted heavily now although additional DDL types may be added in the future:\n1. `CREATE INDEX CONCURRENTLY IF NOT EXISTS`\n1. `DROP INDEX CONCURRENTLY IF EXISTS` (Postgres 13+)\n1. `ALTER TABLE ADD CONSTRAINT` when it's part of an anonymous block that checks for the existence of the constraint.\n\n### Requires psql\nFor now, Anchor Migrations assumes you're using psql to apply migrations, and that psql can connect to your target database.\n\n### What's out of scope for Anchor Migrations?\nAnchor Migrations are non-blocking and idempotent.\n\nFor destructive operations like table drops, column drops, or migrations with code dependencies, Anchor Migrations should not be used.\n\nRemove application code references first, before making schema changes.\n\nUse Strong Migrations or similar to help guide that process, and use regular Rails migrations.\n\nSome of those destructive operations are:\n1. `DROP TABLE`\n1. Adding a non-nullable column, or a column with a default value\n1. Dropping constraints\n1. Adding initially valid constraints\n1. Add or dropping indexes without the CONCURRENTLY keyword\n\n[^docs]: \u003chttps://www.postgresql.org/docs/current/runtime-config-client.html\u003e\n[^tbd]: \u003chttps://trunkbaseddevelopment.com\u003e\n[^strong]: \u003chttps://github.com/ankane/strong_migrations\u003e\n\n## Testing Integration in Rails\nAdd to the project's Gemfile, then run `bundle`.\n\nOnce installed, test that it works by running:\n```sh\nbundle exec anchor help\n```\n\n## Building, Testing, Publishing\n```sh\ngem build anchor_migrations.gemspec\ngem install ./anchor_migrations-0.1.0.gem\nbundle exec rake test\ngem push anchor_migrations-0.1.0.gem\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyatkinson%2Fanchor_migrations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandyatkinson%2Fanchor_migrations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyatkinson%2Fanchor_migrations/lists"}