Files
core2026/packages/infra/prisma/migrations
..

Game database migration

game.prisma의 운영·검증 database는 이 디렉터리의 migration chain으로 준비합니다. prisma db push는 정식 migration을 대신하지 않습니다.

적용

Git에서 제외된 환경 파일 또는 secret 주입으로 DATABASE_URL을 설정합니다.

pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
pnpm --filter @sammo-ts/infra prisma:migrate:status:game

20250101000000_init_game_schema가 game schema의 baseline입니다. Baseline과 적용된 migration 파일·checksum은 수정하지 않고 새 timestamp migration을 추가합니다.

빈 DB 검증

전용 임시 PostgreSQL database에 deploy를 두 번 실행합니다. 첫 실행은 전체 chain을 적용하고 두 번째 실행은 No pending migrations to apply여야 합니다.

최소 확인 항목은 다음과 같습니다.

  • _prisma_migrations의 모든 행이 완료 상태
  • world_state, nation, city, general, message, troop
  • general_turn, nation_turn과 revision·lease field
  • input_event, turn_daemon_lease
  • read_model_revision, read_model_outbox, read_model_revision_meta, web_push_outbox
  • 두 outbox의 available_at, locked_at, delivered_at, created_at은 millisecond 정밀도 timestamp without time zone을 유지한다. 이 migration 이후 신규 값과 pending/dispatcher 운영 계약은 UTC wall 값으로 통일하되, 이미 전달된 과거 행의 표시용 시각 전체를 일괄 재해석하지 않는다.
  • read_model_revision_meta.id=1coverage_version=0
  • diplomacy, event, log_entry, error_log
  • auction, board, vote, yearbook, archive와 inheritance table
  • vote_poll.created_at/updated_at, vote.created_at, vote_comment.created_at의 신규 raw-SQL fallback은 KST game session에서도 UTC wall 값을 기록한다. 현재 writer는 JavaScript Date를 명시하고, 이전 writer 형태의 column 생략도 새 default로 안전해야 한다. 기존 vote timestamp는 Prisma UTC와 raw KST 출처를 구분할 표식이 없어 소급 이동하지 않는다.
  • nation.chief_general_id
  • city.trade nullable, city.trust REAL
  • auction_bid.meta JSONB NOT NULL
  • traffic_period, traffic_period_general과 unique key
  • general_access_batch, primary key와 created_at index
  • select_npc_token, select_npc_token_valid_until_idx
  • general_user_id_key

검증이 끝나면 이름을 직접 확인한 임시 database와 role만 제거합니다. 공유 database나 Compose volume을 삭제하지 않습니다.

Game outbox UTC-wall populated upgrade 검증

Git에서 제외된 전용 PostgreSQL URL을 주입해 target 직전 migration chain부터 실제 data upgrade와 두 번째 deploy no-op까지 검증합니다.

GAME_OUTBOX_MIGRATION_TEST_DATABASE_URL=... \
  pnpm --filter @sammo-ts/infra verify:migration:outbox-utc

검증기는 실행별 소유권 comment가 있는 schema만 만들고 정리합니다. KST DB default로 생성된 ReadModel created_at의 UTC-wall 변환, 기존 JavaScript UTC WebPush created_at 보존, 두 pending outbox의 requeue·lease 해제, delivered 행 보존, target checksum과 이전 DML shape 호환성을 확인합니다. 이전 binary를 별도 build해 실행하거나 down migration을 제공한다는 뜻은 아닙니다.

운영 release-controller의 게임 migration 명령은 profile runtime URL을 바꾸지 않고 migration 연결에만 options=-c TimeZone=Asia/Seoul을 추가합니다. 이미 명시된 DATABASE_URL option/query, PGOPTIONS 또는 PGTZ가 다른 timezone을 요구하면 마지막 옵션으로 덮지 않고 migration 시작 전에 실패합니다. 그 다음 변경하지 않은 원본 profile URL로 current_setting('TimeZone')을 조회해 기존 writer session이 실제로 Asia/Seoul인지 확인한 뒤에만, KST option을 고정한 별도 URL로 Prisma migration을 실행합니다. 이는 이미 배포된 migration checksum을 보존하면서 legacy game wall-clock provenance가 다른 과거 시각을 0700 migration이 잘못 재해석하는 일을 막습니다.

실제 fail-closed 경계는 timezone option이 없는 일회성 UTC-default role URL을 PROFILE_MIGRATION_UTC_DATABASE_URL로 주입하고 다음처럼 재현합니다. 이 URL은 격리 DB의 disposable role만 사용하며 문서·로그에 값을 남기지 않습니다. marker는 external_fixture로 등록되어 일반 조건부 runner가 일회성 role을 만들거나 이 테스트를 실행하지 않으며, 위 URL을 준비한 명시적 실행에서만 활성화됩니다.

pnpm --filter @sammo-ts/gateway-api test profileMigrationTimezone.integration.test.ts

NPC selection 중복 owner preflight

20260731000000_add_npc_selection_tokengeneral.user_id 중복을 발견하면 token table과 unique index를 만들기 전에 실패합니다. 실제 Prisma 실패 metadata와 운영자 정리 뒤 recovery를 전용 tmpfs PostgreSQL에서 검증합니다.

pnpm --filter @sammo-ts/infra verify:migration:npc-selection

검증기는 target 직전 migration 상태에 synthetic 중복 owner를 넣고 다음 순서를 확인합니다.

  1. migrate deploy가 실패합니다. Prisma 7.2 CLI의 최상위 오류는 내부 owner 진단 대신 current transaction is aborted로 표시됩니다.
  2. migration transaction의 table/index DDL이 남지 않습니다.
  3. target _prisma_migrations에는 finished_at, rolled_back_at, logs가 모두 NULL이고 applied_steps_count=0인 미완료 행이 하나 남습니다.
  4. 재실행은 이 미완료 이력 때문에 P3009로 차단됩니다.
  5. 중복을 정리하고 target을 --rolled-back으로 resolve한 뒤 deploy가 성공합니다.
  6. 두 번째 deploy는 no-op이고 migration status가 clean입니다.

검증기는 postgres:18.4-bookworm container의 데이터 경로를 tmpfs로 mount하며 Docker volume을 만들지 않습니다. 임의 포트와 실행 고유 schema를 사용하고 EXIT/HUP/INT/TERM에서 소유 label을 확인한 정확한 container만 제거합니다.

운영 복구에서는 중복 owner를 임의 삭제하지 말고 진단된 계정과 장수를 확인해 주세요. CLI 오류가 일반 transaction 오류만 표시하면 다음 read-only query로 대상을 확인합니다.

SELECT
    user_id,
    count(*) AS owner_count,
    string_agg(id::TEXT, ',' ORDER BY id) AS general_ids
FROM general
WHERE user_id IS NOT NULL
GROUP BY user_id
HAVING count(*) > 1
ORDER BY user_id;

중복 원인을 정리한 뒤 다음처럼 실패한 target만 rolled-back으로 표시하고 deploy를 다시 실행합니다.

pnpm --filter @sammo-ts/infra exec prisma migrate resolve \
  --rolled-back 20260731000000_add_npc_selection_token \
  --schema prisma/game.prisma
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game

다른 migration을 resolve하거나 중복을 정리하기 전에 resolve하지 말아 주세요. 이 migration에는 성공 적용을 되돌리는 down migration이 없습니다. 성공 뒤 복구가 필요하면 사전 DB backup을 복원하거나 별도 forward migration을 작성해 주세요.