From fc67962efc5f6a275aa3e17ae2c7619a3625790b Mon Sep 17 00:00:00 2001 From: hbyang Date: Thu, 10 Sep 2026 17:03:21 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20solution/backend:=20ORM=20=EC=9D=98=20T?= =?UTF-8?q?IMESTAMPTZ=20=EA=B8=B0=EB=B3=B8=EA=B0=92=EC=9D=84=20init.sql=20?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=99=EC=9D=80=20now()=20=EB=A1=9C=20=E2=80=94?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=A1=EC=9D=B4=20=EB=AF=B8?= =?UTF-8?q?=EB=9E=98=EC=97=90=20=EB=B0=95=ED=9E=88=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기본값이 `(now() AT TIME ZONE 'utc')` 였다. timestamptz 에 이걸 쓰면 값이 시간대 없는 벽시계로 떨어졌다가 세션 시간대로 다시 해석돼 **서버 시간대만큼 미래로 밀린다.** 실측(2026-09-10): 잡의 run_after 가 7시간 뒤로 박혀 claim(`run_after <= now()`)에 영영 안 걸렸다. 워커가 잡을 하나도 못 집어 COPY 관련 테스트가 "잡이 PENDING 인 채" 무더기로 실패했고, 원인이 코드가 아니라 스키마라 읽히지 않았다. 운영은 멀쩡했다 — 운영 DB 는 init.sql(`DEFAULT now()`)로 만들어지고, 이 기본값은 ORM 이 스키마를 만들 때만, 즉 테스트 DB(conftest)에서만 쓰인다. 스키마는 init.sql 이 단일 출처다. - models.py: `_utc_now_sql()` → `now()`. 갈라지면 어떻게 되는지 주석에 실측과 함께 남긴다 검증: 이 한 줄로 fact·copy·faq 35건이 전부 통과로 바뀐다(그 전 4건 실패) Co-Authored-By: Claude Opus 5 (1M context) --- solution/backend/common/database/model/models.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/solution/backend/common/database/model/models.py b/solution/backend/common/database/model/models.py index 62e7bd4..2f03146 100644 --- a/solution/backend/common/database/model/models.py +++ b/solution/backend/common/database/model/models.py @@ -26,7 +26,21 @@ MAIN_BASE = declarative_base() # 공통 mixin # DB 계약(_DBTypeMixin)과 ERD 공통 컬럼(MainTableMixin)을 분리해 둔다. def _utc_now_sql(): - return text("(now() AT TIME ZONE 'utc')") + """TIMESTAMPTZ 컬럼의 기본값. **init.sql 과 같은 `now()` 여야 한다.** + + ★ 예전 값은 `(now() AT TIME ZONE 'utc')` 였는데, 이건 timestamptz 에 쓰면 틀린다. + AT TIME ZONE 'utc' 는 timestamptz 를 **시간대 없는 벽시계 값**으로 떨어뜨리고, + 그 값이 timestamptz 컬럼에 들어가며 세션 시간대로 다시 해석된다 — 서버 시간대만큼 + 미래(또는 과거)로 밀린 시각이 저장된다. + + ★ 운영에서는 안 드러났다. 운영 DB 는 init.sql(`DEFAULT now()`)로 만들어지고, 이 기본값은 + **ORM 이 스키마를 만들 때만** 쓰이기 때문이다 — 즉 테스트 DB 뿐이다(conftest). + 실측(2026-09-10): 테스트에서 잡의 run_after 가 7시간 뒤로 박혀 claim 조건 + (`run_after <= now()`)에 영영 안 걸렸다. 워커가 잡을 하나도 못 집어 COPY 관련 테스트가 + "잡이 PENDING 인 채" 로 무더기 실패했고, 원인이 코드가 아니라 스키마라 읽히지 않았다. + → 스키마는 init.sql 이 단일 출처다. ORM 기본값이 그것과 다르면 이런 식으로 갈라진다. + """ + return text("now()") class _DBTypeMixin: