fix: bound repository collection identity projection
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ed7-828d-7ca0-a8d4-0c3e5a0c4102
This commit is contained in:
parent
4cef58cd98
commit
3578dc2f32
10 changed files with 3331 additions and 9 deletions
|
|
@ -13,7 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|||
from sqlalchemy import case, func, or_, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload
|
||||
from sqlalchemy.orm import joinedload, noload
|
||||
|
||||
from api.config import settings
|
||||
from api.database import get_session
|
||||
|
|
@ -57,6 +57,7 @@ from api.services.repository_identity import stage_initial_repository_identity
|
|||
from api.services.repository_aliases import (
|
||||
RepositorySlugResolution,
|
||||
affected_slug_predicate,
|
||||
repository_resolutions_for_repositories,
|
||||
repository_resolution_for_id,
|
||||
resolve_repository_slug,
|
||||
stale_external_references,
|
||||
|
|
@ -126,17 +127,20 @@ async def list_repos(
|
|||
response.headers["Cache-Control"] = "max-age=60, stale-while-revalidate=30"
|
||||
q = (
|
||||
select(ManagedRepo)
|
||||
.options(noload(ManagedRepo.goals))
|
||||
.options(
|
||||
noload("*"),
|
||||
joinedload(ManagedRepo.domain).noload("*"),
|
||||
)
|
||||
.order_by(ManagedRepo.name)
|
||||
)
|
||||
if domain:
|
||||
domain_result = await session.execute(select(Domain).where(Domain.slug == domain))
|
||||
domain_obj = domain_result.scalar_one_or_none()
|
||||
if domain_obj is None:
|
||||
domain_result = await session.execute(select(Domain.id).where(Domain.slug == domain))
|
||||
domain_id = domain_result.scalar_one_or_none()
|
||||
if domain_id is None:
|
||||
raise HTTPException(status_code=404, detail=f"Domain '{domain}' not found")
|
||||
q = q.where(
|
||||
or_(
|
||||
ManagedRepo.domain_id == domain_obj.id,
|
||||
ManagedRepo.domain_id == domain_id,
|
||||
ManagedRepo.secondary_domains.contains([domain]),
|
||||
)
|
||||
)
|
||||
|
|
@ -147,7 +151,9 @@ async def list_repos(
|
|||
if business_stake:
|
||||
q = q.where(ManagedRepo.business_stake.contains([business_stake]))
|
||||
result = await session.execute(q)
|
||||
return await _project_repo_reads(session, list(result.scalars().all()))
|
||||
repositories = list(result.scalars().all())
|
||||
resolutions = await repository_resolutions_for_repositories(session, repositories)
|
||||
return await _project_repo_reads(session, repositories, resolutions=resolutions)
|
||||
|
||||
|
||||
@router.post("/", response_model=RepoRead, status_code=status.HTTP_201_CREATED)
|
||||
|
|
@ -944,6 +950,7 @@ async def _project_repo_reads(
|
|||
repositories: list[ManagedRepo],
|
||||
*,
|
||||
requested: RepositorySlugResolution | None = None,
|
||||
resolutions: dict[uuid.UUID, RepositorySlugResolution] | None = None,
|
||||
include_stale_external: bool = False,
|
||||
) -> list[RepoRead]:
|
||||
projections = await _sbom_projection_map()
|
||||
|
|
@ -952,7 +959,11 @@ async def _project_repo_reads(
|
|||
resolution = (
|
||||
requested
|
||||
if requested is not None and requested.repo.id == repository.id
|
||||
else await repository_resolution_for_id(session, repository)
|
||||
else (
|
||||
resolutions[repository.id]
|
||||
if resolutions is not None
|
||||
else await repository_resolution_for_id(session, repository)
|
||||
)
|
||||
)
|
||||
read = RepoRead.model_validate(repository)
|
||||
read = read.model_copy(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from dataclasses import dataclass
|
|||
from fastapi import HTTPException
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload
|
||||
|
||||
from api.models.fabric_graph import FabricGraphEdge, FabricGraphImport, FabricGraphNode
|
||||
from api.models.managed_repo import ManagedRepo
|
||||
|
|
@ -95,6 +96,79 @@ async def resolve_repository_slug(
|
|||
)
|
||||
|
||||
|
||||
async def repository_resolutions_for_repositories(
|
||||
session: AsyncSession,
|
||||
repositories: list[ManagedRepo],
|
||||
) -> dict[uuid.UUID, RepositorySlugResolution]:
|
||||
"""Resolve a repository collection with one slug-registry query.
|
||||
|
||||
Collection callers already hold canonical ``ManagedRepo`` rows. Resolving
|
||||
every row through :func:`resolve_repository_slug` would issue two identity
|
||||
queries per repository. Load the complete alias sets once instead while
|
||||
retaining the pre-registry compatibility behavior.
|
||||
"""
|
||||
|
||||
if not repositories:
|
||||
return {}
|
||||
|
||||
repository_ids = [repository.id for repository in repositories]
|
||||
records = list(
|
||||
(
|
||||
await session.execute(
|
||||
select(RepositorySlug)
|
||||
.options(
|
||||
noload(RepositorySlug.repo),
|
||||
noload(RepositorySlug.source_operation),
|
||||
)
|
||||
.where(RepositorySlug.repo_id.in_(repository_ids))
|
||||
.order_by(RepositorySlug.repo_id, RepositorySlug.kind.desc(), RepositorySlug.slug)
|
||||
)
|
||||
).scalars()
|
||||
)
|
||||
records_by_repository: dict[uuid.UUID, list[RepositorySlug]] = {
|
||||
repository_id: [] for repository_id in repository_ids
|
||||
}
|
||||
for record in records:
|
||||
records_by_repository[record.repo_id].append(record)
|
||||
|
||||
resolutions: dict[uuid.UUID, RepositorySlugResolution] = {}
|
||||
for repository in repositories:
|
||||
repository_records = records_by_repository[repository.id]
|
||||
requested_record = next(
|
||||
(record for record in repository_records if record.slug == repository.slug),
|
||||
None,
|
||||
)
|
||||
if requested_record is None:
|
||||
# Compatibility for databases upgraded before the identity backfill.
|
||||
resolutions[repository.id] = RepositorySlugResolution(
|
||||
repo=repository,
|
||||
requested_slug=repository.slug,
|
||||
canonical_slug=repository.slug,
|
||||
slug_status="canonical",
|
||||
aliases=(),
|
||||
)
|
||||
continue
|
||||
|
||||
canonicals = [record for record in repository_records if record.kind == "canonical"]
|
||||
if len(canonicals) != 1 or canonicals[0].slug != repository.slug:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Repository '{repository.id}' has inconsistent canonical slug state",
|
||||
)
|
||||
resolutions[repository.id] = RepositorySlugResolution(
|
||||
repo=repository,
|
||||
requested_slug=repository.slug,
|
||||
canonical_slug=repository.slug,
|
||||
slug_status=requested_record.kind,
|
||||
aliases=tuple(
|
||||
record.slug for record in repository_records if record.kind == "alias"
|
||||
),
|
||||
source_operation_id=requested_record.source_operation_id,
|
||||
)
|
||||
|
||||
return resolutions
|
||||
|
||||
|
||||
async def canonicalize_repository_slug(session: AsyncSession, value: str) -> str:
|
||||
"""Canonicalize a value only when it is a registered repository identity."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue