How to add a UniqueConstraint concurrently in Django

Timmer O'Phelan
2 min readOct 5, 2021

--

I spent some time searching for this answer before I realized I’d have to figure it out for myself. Now you don't have to. You’re welcome.

I came across a race condition that managed to sidestep my best efforts to keep a set of conditions and fields unique for one of my models. To solve this, decided to leverage Django’s UniqueConstraint:

class Baz(models.Model):
foo = models.ForeignKey(Foo)
bar = models.ForeignKey(Bar)
is_active = models.Boolean()
class Meta:
constraints = [
models.UniqueConstraint(
conditions=Q(is_active=True)
fields=["foo", "bar"],
name="unique_active_foo_bar"
)
]

After running ./manage.py makemigrations I got a migrations file that included this operation:

# migrations 0065migrations.AddConstraint(
model_name="bar",
constraint=models.UniqueConstraint(
condition=models.Q("is_active", True),
fields=("foo", "bar"),
name="unique_active_foo_bar",
),
),

This would be exactly what I wanted, except the table I’m migrating contains millions of rows. Under the hood, UniqueConstraint (for postgres) creates a UNIQUE INDEX meaning this would migration would lock and prevent any writes until the index was created. To avoid this, I wanted to apply the equivalent of migrations.addIndexConcurrently to my AddContraint. But after some searching around and reading the Django docs, I wasn’t able to find a prebuilt way to achieve this.

However, I did learn about ./manage.py sqlmigrate along the way. It would allow me to see the SQL my migration would generate. After running it on my migration with ./manage.py sqlmigrate app 0065 I saw the resulting SQL simply created a unique index:

CREATE UNIQUE INDEX "unique_active_foo_bar" ON "stock_job" ("foo", "bar") WHERE ("is_active" = true);

And after running ./manage.py sqlmigrate app 0065 --backwards I was able to get the SQL for the reserve migration:

DROP INDEX IF EXISTS "unique_active_foo_bar"

I then replaced the operation in my migration file with a SeparateDatabaseAndStatecommand so I could manage my state changes and database changes separately, and added myCONCURRENTLY requirement.

migrations.SeparateDatabaseAndState(
database_operations = [
migrations.RunSQL(
sql="""
CREATE UNIQUE INDEX CONCURRENTLY "unique_active_foo_bar"
ON "app_baz" ("foo", "bar") WHERE ("is_active" = true);
""",
reverse_sql="""
DROP INDEX CONCURRENTLY IF EXISTS
"unique_active_foo_bar"
""",
], state_operations=[
migrations.AddConstraint(
model_name="bar",
constraint=models.UniqueConstraint(
condition=models.Q("is_active", True),
fields=("foo", "bar"),
name="unique_active_foo_bar",
),
),
],
)

I hope this saves someone just a little bit of time.

--

--