diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 85d04ed9c..eef4ddfee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Release notes ============= +Version v38.5.0 +--------------------- + +- fix: Make package_url field unique for PackageV2 + Version v38.4.0 --------------------- diff --git a/vulnerabilities/migrations/0122_auto_20260415_1155.py b/vulnerabilities/migrations/0122_auto_20260415_1155.py new file mode 100644 index 000000000..0f9463302 --- /dev/null +++ b/vulnerabilities/migrations/0122_auto_20260415_1155.py @@ -0,0 +1,36 @@ +from django.db import migrations +from django.db.models import F, Window +from django.db.models.functions import RowNumber + + +def remove_duplicate_package_urls(apps, schema_editor): + PackageV2 = apps.get_model("vulnerabilities", "PackageV2") + + duplicates = ( + PackageV2.objects + .annotate( + rn=Window( + expression=RowNumber(), + partition_by=[F("package_url")], + order_by=F("id").desc(), + ) + ) + .filter(rn__gt=1) + ) + + BATCH_SIZE = 1000 + ids = list(duplicates.values_list("id", flat=True)) + + for i in range(0, len(ids), BATCH_SIZE): + PackageV2.objects.filter(id__in=ids[i:i+BATCH_SIZE]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0121_advisoryv2_is_latest_alter_advisoryv2_advisory_id_and_more"), + ] + + operations = [ + migrations.RunPython(remove_duplicate_package_urls, migrations.RunPython.noop), + ] \ No newline at end of file diff --git a/vulnerabilities/migrations/0123_alter_packagev2_options_alter_packagev2_package_url_and_more.py b/vulnerabilities/migrations/0123_alter_packagev2_options_alter_packagev2_package_url_and_more.py new file mode 100644 index 000000000..6183a363e --- /dev/null +++ b/vulnerabilities/migrations/0123_alter_packagev2_options_alter_packagev2_package_url_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.2.11 on 2026-04-15 11:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0122_auto_20260415_1155"), + ] + + operations = [ + migrations.AlterModelOptions( + name="packagev2", + options={ + "ordering": [ + "type", + "namespace", + "name", + "version_rank", + "version", + "qualifiers", + "subpath", + ] + }, + ), + migrations.AlterField( + model_name="packagev2", + name="package_url", + field=models.CharField( + db_index=True, + help_text="The Package URL for this package.", + max_length=1000, + unique=True, + ), + ), + migrations.AlterUniqueTogether( + name="packagev2", + unique_together={("type", "namespace", "name", "version", "qualifiers", "subpath")}, + ), + migrations.AddIndex( + model_name="packagev2", + index=models.Index( + fields=["type", "namespace", "name"], name="vulnerabili_type_ca0efc_idx" + ), + ), + migrations.AddIndex( + model_name="packagev2", + index=models.Index( + fields=["type", "namespace", "name", "qualifiers", "subpath"], + name="vulnerabili_type_c98c98_idx", + ), + ), + migrations.AddIndex( + model_name="packagev2", + index=models.Index( + fields=["type", "namespace", "name", "version"], name="vulnerabili_type_1af1cc_idx" + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 11f4ad61e..c874db4e4 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3490,6 +3490,7 @@ class PackageV2(PackageURLMixin): null=False, help_text="The Package URL for this package.", db_index=True, + unique=True, ) plain_package_url = models.CharField( @@ -3520,6 +3521,24 @@ class PackageV2(PackageURLMixin): db_index=True, ) + class Meta: + unique_together = ["type", "namespace", "name", "version", "qualifiers", "subpath"] + ordering = ["type", "namespace", "name", "version_rank", "version", "qualifiers", "subpath"] + indexes = [ + # Index for getting al versions of a package + models.Index(fields=["type", "namespace", "name"]), + models.Index(fields=["type", "namespace", "name", "qualifiers", "subpath"]), + # Index for getting a specific version of a package + models.Index( + fields=[ + "type", + "namespace", + "name", + "version", + ] + ), + ] + def __str__(self): return self.package_url