22import json
33from itertools import groupby
44from operator import attrgetter
5- from typing import TypedDict
5+ from typing import NamedTuple , TypedDict
66
77import django .db .models .deletion
88from django .apps .registry import Apps
@@ -42,18 +42,33 @@ def migrate_scans_forward(apps: Apps, _: object) -> None:
4242 "vcs_provider" ,
4343 ).annotate (last_scanned_at = Max ("created_at" ))
4444
45- repositories = {
46- ( summary [ "project_id" ], summary [ "repository_url" ]): Repository . objects . create (
45+ repositories_to_create = [
46+ Repository (
4747 project_id = summary ["project_id" ],
4848 url = summary ["repository_url" ],
4949 vcs_provider = summary ["vcs_provider" ],
5050 last_scanned_at = summary ["last_scanned_at" ],
5151 )
5252 for summary in legacy_scans_summaries
53+ ]
54+ Repository .objects .bulk_create (repositories_to_create )
55+ repositories = {
56+ (repository .project_id , repository .url ): repository
57+ for repository in repositories_to_create
5358 }
5459
55- # Oldest-first per project so the newest scan wins on hash collisions
56- legacy_scans = LegacyScan .objects .order_by ("project_id" , "created_at" ).iterator ()
60+ class PerFeatureScanKey (NamedTuple ):
61+ feature_id : int
62+ repository_id : int
63+ code_references_hash : str
64+
65+ # Iteration is oldest-first, so overwriting on key collision means the
66+ # newest legacy scan wins for a given (feature, repository, hash).
67+ per_feature_rows : dict [PerFeatureScanKey , models .Model ] = {}
68+
69+ legacy_scans = LegacyScan .objects .order_by ("project_id" , "created_at" ).iterator (
70+ chunk_size = 200 ,
71+ )
5772 grouped_scans = groupby (legacy_scans , key = attrgetter ("project_id" ))
5873 for project_id , project_scans in grouped_scans :
5974 features = {
@@ -64,8 +79,7 @@ def migrate_scans_forward(apps: Apps, _: object) -> None:
6479 )
6580 }
6681 for legacy_scan in project_scans :
67- repository_url = legacy_scan .repository_url
68- repository = repositories [project_id , repository_url ]
82+ repository = repositories [project_id , legacy_scan .repository_url ]
6983
7084 references_by_feature : dict [str , list [StoredCodeReference ]] = {}
7185 for reference in legacy_scan .code_references :
@@ -80,17 +94,26 @@ def migrate_scans_forward(apps: Apps, _: object) -> None:
8094 for feature_name , references in references_by_feature .items ():
8195 if not (feature := features .get ((project_id , feature_name ))):
8296 continue
83- PerFeatureScan .objects .update_or_create (
97+ references_hash = _hash_references (references )
98+ key = PerFeatureScanKey (
99+ feature_id = feature .id ,
100+ repository_id = repository .id ,
101+ code_references_hash = references_hash ,
102+ )
103+ per_feature_rows [key ] = PerFeatureScan (
84104 feature = feature ,
85105 repository = repository ,
86- code_references_hash = _hash_references (references ),
87- defaults = {
88- "revision" : legacy_scan .revision ,
89- "code_references" : references ,
90- "created_at" : legacy_scan .created_at ,
91- },
106+ code_references_hash = references_hash ,
107+ revision = legacy_scan .revision ,
108+ code_references = references ,
109+ created_at = legacy_scan .created_at ,
92110 )
93111
112+ PerFeatureScan .objects .bulk_create (
113+ per_feature_rows .values (),
114+ batch_size = 1000 ,
115+ )
116+
94117
95118def migrate_scans_backward (apps : Apps , _ : object ) -> None :
96119 """Mirror each per-feature row back into the legacy single-table layout."""
0 commit comments