"""Postgres adapter for the ``property_overrides`` read side. Read-only and uow-independent: ``property_overrides`` is committed reference data the ``bulk_upload_finaliser`` Lambda writes at Finalise, long before First Run executes — there is no transactional coupling to the ingestion run, so the standalone reader opens its own short read session per call via the injected session factory (mirroring the composition root's ``lambda: Session(engine)``). Inside a Unit of Work the overrides must instead be read on the UoW's *own* session (``OpenSessionPropertyOverridesReader``): a second session opened concurrently checks out a second connection, which deadlocks the modelling_e2e Lambda's single-connection pool while the UoW's read transaction is still open. Reading committed reference data inside the UoW's transaction is correct — it sees the same committed rows — and keeps the invocation on one connection. """ from __future__ import annotations from collections.abc import Callable from sqlmodel import Session, col, select from infrastructure.postgres.property_override_table import PropertyOverrideRow from repositories.property.property_overrides_reader import ( PropertyOverridesReader, ResolvedPropertyOverride, ResolvedPropertyOverrides, ) def _resolve_overrides(session: Session, property_id: int) -> ResolvedPropertyOverrides: rows = session.exec( select(PropertyOverrideRow).where( col(PropertyOverrideRow.property_id) == property_id ) ).all() return ResolvedPropertyOverrides( rows=tuple( ResolvedPropertyOverride( override_component=row.override_component, building_part=row.building_part, override_value=row.override_value, ) for row in rows ) ) def _resolve_overrides_many( session: Session, property_ids: list[int] ) -> dict[int, ResolvedPropertyOverrides]: """Resolve overrides for many Properties in one query. Returns an entry for every requested id; a Property with no rows resolves to an empty snapshot.""" rows = session.exec( select(PropertyOverrideRow).where( col(PropertyOverrideRow.property_id).in_(property_ids) ) ).all() grouped: dict[int, list[ResolvedPropertyOverride]] = { property_id: [] for property_id in property_ids } for row in rows: grouped.setdefault(row.property_id, []).append( ResolvedPropertyOverride( override_component=row.override_component, building_part=row.building_part, override_value=row.override_value, ) ) return { property_id: ResolvedPropertyOverrides(rows=tuple(overrides)) for property_id, overrides in grouped.items() } class PropertyOverridesPostgresReader(PropertyOverridesReader): """Opens its own short read session per call — for standalone use outside a Unit of Work, where there is no shared session/connection to reuse.""" def __init__(self, session_factory: Callable[[], Session]) -> None: self._session_factory = session_factory def overrides_for(self, property_id: int) -> ResolvedPropertyOverrides: with self._session_factory() as session: return _resolve_overrides(session, property_id) def overrides_for_many( self, property_ids: list[int] ) -> dict[int, ResolvedPropertyOverrides]: """Every requested Property's resolved overrides in one query — the batch form of ``overrides_for``. The returned dict has an entry for every requested id; a Property with no rows resolves to an empty snapshot (exactly as ``overrides_for`` returns for a Property with no rows).""" with self._session_factory() as session: return _resolve_overrides_many(session, property_ids) class OpenSessionPropertyOverridesReader(PropertyOverridesReader): """Reads on a caller-owned, already-open session without closing it — for use inside a Unit of Work so resolving overrides reuses the UoW's single connection instead of checking out a second one.""" def __init__(self, session: Session) -> None: self._session = session def overrides_for(self, property_id: int) -> ResolvedPropertyOverrides: return _resolve_overrides(self._session, property_id)