Skip to content

Commit c9bb2e0

Browse files
committed
refactor(backfill): use skills.latest_semver join and add --random flag
Two related changes to make the scan-report backfill usable at scale and able to produce representative samples: - Replace the ROW_NUMBER() OVER (PARTITION BY skill_id ORDER BY created_at DESC) subquery with a direct join on the denormalized skills.latest_semver pointer. The window-function variant trips the 30s statement_timeout on the current versions table; the new query runs in well under a second. - Add a --random flag that swaps the ORDER BY from (org_slug, name) to random(). Alphabetical ordering concentrated --limit N batches on a handful of orgs (e.g. 77% of the first 200-skill run landed on a single prolific publisher); random order spreads the same sample across ~80 orgs with no single org over 10%, which is what we want for the distribution stats in #329. Made-with: Cursor
1 parent de95818 commit c9bb2e0

1 file changed

Lines changed: 35 additions & 26 deletions

File tree

server/src/decision_hub/scripts/backfill_scan_reports.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,22 @@
4141
MAX_CONSECUTIVE_ERRORS = 10
4242

4343

44-
def _find_skills_needing_scan(engine: sa.engine.Engine, *, limit: int | None) -> list[dict]:
45-
"""Return skills whose latest version has no scan_report."""
46-
latest = (
44+
def _find_skills_needing_scan(engine: sa.engine.Engine, *, limit: int | None, random_order: bool = False) -> list[dict]:
45+
"""Return skills whose latest version has no scan_report.
46+
47+
When ``random_order`` is True, results are shuffled at the SQL level so a
48+
``--limit N`` batch is a representative sample of the catalog instead of
49+
the alphabetically-first N skills (which skews heavily to a few orgs).
50+
51+
Uses the denormalized ``skills.latest_semver`` pointer to join the latest
52+
version directly — avoids a ``ROW_NUMBER() OVER (...)`` over the full
53+
``versions`` table that blows through the 30s statement_timeout at scale.
54+
"""
55+
stmt = (
4756
sa.select(
4857
skill_versions_table.c.id.label("version_id"),
49-
skill_versions_table.c.skill_id,
5058
skill_versions_table.c.s3_key,
5159
skill_versions_table.c.semver,
52-
sa.func.row_number()
53-
.over(
54-
partition_by=skill_versions_table.c.skill_id,
55-
order_by=skill_versions_table.c.created_at.desc(),
56-
)
57-
.label("rn"),
58-
)
59-
).subquery("latest")
60-
61-
stmt = (
62-
sa.select(
63-
latest.c.version_id,
64-
latest.c.s3_key,
65-
latest.c.semver,
6660
organizations_table.c.slug.label("org_slug"),
6761
skills_table.c.name.label("skill_name"),
6862
)
@@ -72,20 +66,26 @@ def _find_skills_needing_scan(engine: sa.engine.Engine, *, limit: int | None) ->
7266
skills_table.c.org_id == organizations_table.c.id,
7367
)
7468
.join(
75-
latest,
69+
skill_versions_table,
7670
sa.and_(
77-
skills_table.c.id == latest.c.skill_id,
78-
latest.c.rn == 1,
71+
skill_versions_table.c.skill_id == skills_table.c.id,
72+
skill_versions_table.c.semver == skills_table.c.latest_semver,
7973
),
8074
)
8175
.outerjoin(
8276
scan_reports_table,
83-
scan_reports_table.c.version_id == latest.c.version_id,
77+
scan_reports_table.c.version_id == skill_versions_table.c.id,
8478
)
8579
)
86-
.where(scan_reports_table.c.id.is_(None))
87-
.order_by(organizations_table.c.slug, skills_table.c.name)
80+
.where(
81+
scan_reports_table.c.id.is_(None),
82+
skills_table.c.latest_semver.is_not(None),
83+
)
8884
)
85+
if random_order:
86+
stmt = stmt.order_by(sa.func.random())
87+
else:
88+
stmt = stmt.order_by(organizations_table.c.slug, skills_table.c.name)
8989
if limit:
9090
stmt = stmt.limit(limit)
9191

@@ -107,6 +107,12 @@ def main() -> None:
107107
parser.add_argument("--dry-run", action="store_true", help="Count only, don't scan")
108108
parser.add_argument("--resume", action="store_true", help="Skip already-scanned (default behavior)")
109109
parser.add_argument("--delay", type=float, default=0.0, help="Seconds to wait between submitting scans")
110+
parser.add_argument(
111+
"--random",
112+
dest="random_order",
113+
action="store_true",
114+
help="Pick skills in random order instead of alphabetical (for representative samples)",
115+
)
110116
args = parser.parse_args()
111117

112118
settings = create_settings()
@@ -118,8 +124,11 @@ def main() -> None:
118124
endpoint_url=settings.s3_endpoint_url,
119125
)
120126

121-
logger.info("Finding skills needing scan reports...")
122-
skills = _find_skills_needing_scan(engine, limit=args.limit)
127+
logger.info(
128+
"Finding skills needing scan reports ({})...",
129+
"random order" if args.random_order else "alphabetical",
130+
)
131+
skills = _find_skills_needing_scan(engine, limit=args.limit, random_order=args.random_order)
123132
logger.info("Found {} skills needing scan reports", len(skills))
124133

125134
if args.dry_run:

0 commit comments

Comments
 (0)