from sqlalchemy import select from sqlalchemy.dialects.postgresql import insert as pg_insert from common.database.db_session_manager import DB_SESSION_MNG from common.database.model.models import place_itineraries from common.utils.gtime import GTime class PlaceItineraryCRUD: async def list_by_place(self, db, place_id): """업장의 일정 전부(기간별 한 행). 정렬은 기간 이름 순이 아니라 저장 순이 아니다 — 화면 탭 순서는 읽는 쪽(`snapshot._local_contents`)이 DURATIONS 순으로 정한다.""" return await DB_SESSION_MNG.execute( db, select(place_itineraries).where( place_itineraries.place_id == place_id, place_itineraries.deleted == False, # noqa: E712 ), ) async def upsert(self, db, values: dict): """업장 × 기간 한 행의 삽입/갱신. ★ `uq_place_itineraries`(place_id, duration — deleted = false)에 태운다. ON CONFLICT 술어는 인덱스 술어와 **글자 그대로** 같아야 한다. 아니면 포스트그레스가 "no unique or exclusion constraint matching" 으로 거절한다 — 표도 컬럼도 멀쩡해서 눈으로는 원인이 안 보인다(`local_content_crud.upsert_kind` 주석과 같은 함정). """ stmt = pg_insert(place_itineraries).values(**values) stmt = stmt.on_conflict_do_update( index_elements=[place_itineraries.place_id, place_itineraries.duration], index_where=(place_itineraries.deleted == False), # noqa: E712 set_={ "body": stmt.excluded.body, "generated_by": stmt.excluded.generated_by, "model": stmt.excluded.model, "generated_at": stmt.excluded.generated_at, "updated_at": GTime.UTC(), }, ) return await DB_SESSION_MNG.add(db, stmt)