PostgreSQL zero-downtime migrations made easy
Go to file
Andrew Farries c76ea9ce48
Add support for adding a foreign key constraint to an existing column (#82)
Add support for adding a foreign key constraint to an existing column.
Such a migration looks like:

```json
{
  "name": "21_add_foreign_key_constraint",
  "operations": [
    {
      "set_foreign_key": {
        "table": "posts",
        "column": "user_id",
        "references": {
          "table": "users",
          "column": "id"
        },
        "up": "(SELECT CASE WHEN EXISTS (SELECT 1 FROM users WHERE users.id = user_id) THEN user_id ELSE NULL END)",
        "down": "user_id"
      }
    }
  ]
}
```

This migration adds a foreign key constraint to the `user_id` column in
the `posts` table, referencing the `id` column in the `users` table.

The implementation is similar to the **set not null** and **change
column type** operations:

* On `Start`:
* Create a new column, duplicating the one to which the FK constraint
should be added.
* The new column has the foreign key constraint added as `NOT VALID` to
avoid taking a long lived `SHARE ROW EXCLUSIVE` lock (see
[here](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680#00dc)).
* Backfill the new column with values from the existing column,
rewriting values using the `up` SQL.
* Create a trigger to populate the new column when values are written to
the old column, converting values with `up`.
* Create a trigger to populate the old column when values are written to
the new column, converting values with `down`.
* On `Complete`
  * Validate the foreign key constraint.
  * Remove triggers
  * Drop the old column
  * Rename the new column to the old column name.
* Rename the foreign key constraint to be consistent with the new name
of the column.
* On `Rollback`
* Remove the new column and both triggers. Removing the new column also
removes the foreign key constraint on it.

The `up` SQL in this operation is critical. The old column does not have
a foreign key constraint imposed on it after `Start` as that would
violate the guarantee that `pg-roll` does not make changes to the
existing schema. The `up` SQL therefore needs to take into account that
not all rows inserted into the old schema will have a valid foreign key.
In the example `json` above, the `up` SQL ensures that values for which
there is no corresponding user in the `users` table result in `NULL`
values in the new column. Failure to do this would result in the old
schema failing to insert rows without a valid `user_id`. An alternative
would be to implement data quarantining for these values, as discussed
last week @exekias .
2023-09-11 06:12:59 +01:00
.github Matrix test against supported Postgres versions in CI (#46) 2023-08-17 11:13:31 +01:00
.vscode Add linter to tests (#9) 2023-06-27 16:33:50 +01:00
cmd Add raw SQL operation (#43) 2023-08-30 11:50:59 +02:00
examples Add support for adding a foreign key constraint to an existing column (#82) 2023-09-11 06:12:59 +01:00
pkg Add support for adding a foreign key constraint to an existing column (#82) 2023-09-11 06:12:59 +01:00
.golangci.yml Add linter to tests (#9) 2023-06-27 16:33:50 +01:00
docker-compose.yml Initial commit 2023-06-22 17:30:40 +02:00
go.mod Change module name (#60) 2023-08-22 09:27:58 +01:00
go.sum Upgrade to Go 1 21 (#54) 2023-08-17 07:43:41 +01:00
main.go Change module name (#60) 2023-08-22 09:27:58 +01:00
README.md Add migrations state handling (#7) 2023-06-28 11:10:03 +02:00

pg-roll

⚠️ Under development ⚠️

PostgreSQL zero-downtime migrations made easy.

Getting started (development)

  • Bring a development PostgreSQL up:

    docker compose up
    
  • Initialize pg-roll (first time only):

    go run . init
    
  • Start a migration:

    go run . start examples/01_create_tables.json
    
  • Inspect the results:

    psql postgres://localhost -U postgres
    
    \d+ public.*
    \d+ 01_create_tables.*
    
  • (Optional) Rollback the migration (undo):

    go run . rollback
    
  • Complete the migration:

    go run . complete