Right, so here is a scene I have watched play out more times than I care to admit. Someone new to Rails decides they want to add a column to an existing table. Lovely. Very ambitious. They also want a NOT NULL constraint on it, because they have heard that data integrity is important, which is technically true, and then they write something like this:
class AddFirstAndLastNameToUsers < ActiveRecord::Migration
def change
add_column :users, :first_name, :string, null: false
add_column :users, :last_name, :string, null: false
end
end
And then they run it. And then Postgres tells them, in no uncertain terms, to get stuffed.
You get a PG::NotNullViolation because, and I cannot stress this enough, Postgres is not stupid. It looks at your table, sees that there are already rows in it, realises that your shiny new column would have no value in any of those rows, and quite reasonably refuses to let you turn your database into a liar. It has no idea what the value of the new columns should be for the existing rows, and unlike some people, it is not prepared to just wing it.
Postgres will not let you add a NOT NULL column to a table that already has data, because it refuses to participate in your fantasy that those existing rows can just have nothing in a column that explicitly forbids nothing.
Which, when you think about it, is actually the database doing its job. The constraint says “this column must always have a value,” and you are trying to create it on a table where it immediately would not. That is not a bug. That is the database respecting your own rules more than you do.
The fix, which is embarrassingly simple
The trick is to break the operation into three steps, all of which can live in a single migration so nobody has to file a support ticket about it. First, you add the column without the constraint. Then you backfill the existing rows with some sensible default. Then you add the NOT NULL constraint after everything already has a value. It is the database equivalent of putting your trousers on before your shoes.
For example, say you are adding first and last names to your Devise users, because apparently you launched an application where you did not bother collecting anyone’s name. Bold choice. Here is how you do it without Postgres throwing a tantrum:
class AddFirstAndLastNameToUsers < ActiveRecord::Migration
def up
add_column :users, :first_name, :string
add_column :users, :last_name, :string
execute <<-SQL.strip_heredoc
UPDATE users
SET first_name = '[[UPDATEME]]'
WHERE first_name IS NULL
SQL
execute <<-SQL.strip_heredoc
UPDATE users
SET last_name = '[[UPDATEME]]'
WHERE last_name IS NULL
SQL
change_column :users, :first_name, :string, null: false
change_column :users, :last_name, :string, null: false
end
def down
remove_column :users, :first_name
remove_column :users, :last_name
end
end
Notice you are using up and down instead of change here, because this is a multi-step migration and Rails cannot magically reverse an execute block. If you try to use change for this, you deserve whatever happens next.
The [[UPDATEME]] placeholder is there to remind you to put in an actual sensible default. Maybe it is an empty string, maybe it is “Unknown,” maybe it is the name of your first pet. The point is that every row gets a value before the constraint goes on, so Postgres has nothing to complain about. And Postgres loves having nothing to complain about.
Also worth noting: the SQL WHERE first_name IS NULL clause means this migration is idempotent in the backfill step. If you somehow end up running it twice, or if some rows already have values because of reasons, it will not clobber them. That is the kind of defensive coding that separates people who sleep at night from people who get paged at 3 AM.
The whole point
This is not complicated. It is barely even interesting. But I keep seeing people get bitten by it, so apparently it needs to be written down somewhere.
The database is not being difficult. It is doing exactly what you asked it to do: enforce your constraints. The fact that your migration contradicts those constraints is a you problem, not a Postgres problem. Add the column first, fill in the blanks, then add the constraint. Three steps. One migration. Zero drama.
- Never add a
NOT NULLcolumn directly to a table that already has rows - Add the column, backfill existing data, then apply the constraint in one migration
- Use
up/downinstead ofchangewhen your migration includes raw SQL
Do that, and Postgres will stop yelling at you. Or at least it will stop yelling at you about this. I make no promises about the rest of your schema.