Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/openedx_content/migrations/0004_componenttype_constraint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
# Generated by Django 5.2.11 on 2026-03-02 23:37

from django.db import migrations, models
from django.db.models import Count, Min


def consolidate_duplicate_component_types(apps, schema_editor):
"""
Older installations may have multiple ComponentType rows with the same
(namespace, name) due to a missing unique constraint.

Before we apply the constraint, we need to fix the data by removing the
duplicate entries. For each set of duplicates, keep the earliest row (lowest
ID), repoint any Components that referenced the duplicates to it, then
delete the duplicates so the new unique constraint can be applied cleanly.
"""
ComponentType = apps.get_model("openedx_content", "ComponentType")
Component = apps.get_model("openedx_content", "Component")

duplicate_groups = (
ComponentType.objects.values("namespace", "name").annotate(num=Count("id"), keep_id=Min("id")).filter(num__gt=1)
)

for group in duplicate_groups:
keep_id = group["keep_id"]
duplicate_ids = list(
ComponentType.objects.filter(namespace=group["namespace"], name=group["name"])
.exclude(id=keep_id)
.values_list("id", flat=True)
)
Component.objects.filter(component_type_id__in=duplicate_ids).update(
component_type_id=keep_id,
)
Comment on lines +29 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is entirely reasonable, but I also don't think you can actually create new Components in practice if there are duplicates of the ComponentType because it should fail to fetch a unique result. But anyway, this can't hurt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't think you can actually create new Components in practice if there are duplicates of the ComponentType because it should fail to fetch a unique result

Sorry, I'm not following. Isn't that exactly the bug we're trying to solve, and exactly the problem that was reported on the forum?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was thinking that it was definitely possible to create duplicate ComponentTypes. But when you try to create a Component, I thought it would error out when trying to fetch the type because there are duplicates. But I was forgetting that it's a get_or_create kind of deal, so it would be good for inserting that one Component during the race condition, even if future attempts to create Components of that type would always error out because there are multiple ComponentTypes that match.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in any case, yeah, this code all makes sense. Thank you. 😄

ComponentType.objects.filter(id__in=duplicate_ids).delete()


class Migration(migrations.Migration):
Expand All @@ -9,6 +38,10 @@ class Migration(migrations.Migration):
]

operations = [
migrations.RunPython(
consolidate_duplicate_component_types,
reverse_code=migrations.RunPython.noop,
),
migrations.AddConstraint(
model_name="componenttype",
constraint=models.UniqueConstraint(fields=("namespace", "name"), name="oel_component_type_uniq_ns_n"),
Expand Down