From 34969c40c833b9eed98f42fc358235c56314f9cd Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 15 Apr 2026 17:31:56 +0530 Subject: [PATCH 1/4] make package_url field unique for PackageV2 Signed-off-by: Tushar Goel --- CHANGELOG.rst | 5 ++ .../migrations/0122_auto_20260415_1155.py | 36 +++++++++++ ...ns_alter_packagev2_package_url_and_more.py | 60 +++++++++++++++++++ vulnerabilities/models.py | 19 ++++++ 4 files changed, 120 insertions(+) create mode 100644 vulnerabilities/migrations/0122_auto_20260415_1155.py create mode 100644 vulnerabilities/migrations/0123_alter_packagev2_options_alter_packagev2_package_url_and_more.py 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..8a91be454 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 From ed06995e656b26a42e4e0c6856e490e95b3fa0fe Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 15 Apr 2026 19:36:23 +0530 Subject: [PATCH 2/4] Fix errors Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 8a91be454..c874db4e4 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3490,7 +3490,7 @@ class PackageV2(PackageURLMixin): null=False, help_text="The Package URL for this package.", db_index=True, - unique=True + unique=True, ) plain_package_url = models.CharField( From 660375a893dbd83bf54a6659dbf1755f0d02e2eb Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 24 Apr 2026 17:38:40 +0530 Subject: [PATCH 3/4] Fix models Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index c874db4e4..4efc04766 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3477,7 +3477,8 @@ def from_purl(self, purl: Union[PackageURL, str]): """ Return a new Package given a ``purl`` PackageURL object or PURL string. """ - return PackageV2.objects.create(**purl_to_dict(purl=purl)) + package, _ = PackageV2.objects.get_or_create(**purl_to_dict(purl=purl)) + return package class PackageV2(PackageURLMixin): From 0e24e73cf2a9abd52e7d31ebd6e456a1dae3f348 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 24 Apr 2026 17:46:38 +0530 Subject: [PATCH 4/4] Bump version Signed-off-by: Tushar Goel --- setup.cfg | 2 +- vulnerablecode/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index e1275dae2..71d62d573 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = vulnerablecode -version = 38.4.0 +version = 38.5.0 license = Apache-2.0 AND CC-BY-SA-4.0 # description must be on ONE line https://github.com/pypa/setuptools/issues/1390 diff --git a/vulnerablecode/__init__.py b/vulnerablecode/__init__.py index f8263d4c5..15d335f6a 100644 --- a/vulnerablecode/__init__.py +++ b/vulnerablecode/__init__.py @@ -14,7 +14,7 @@ import git -__version__ = "38.4.0" +__version__ = "38.5.0" PROJECT_DIR = Path(__file__).resolve().parent