diff --git a/packages/infra/package.json b/packages/infra/package.json index 29f566f..204493e 100644 --- a/packages/infra/package.json +++ b/packages/infra/package.json @@ -15,6 +15,8 @@ "typecheck": "tsc -b", "prisma:generate:game": "prisma generate --schema prisma/game.prisma", "prisma:generate:gateway": "prisma generate --schema prisma/gateway.prisma", + "prisma:migrate:deploy:game": "prisma migrate deploy --schema prisma/game.prisma", + "prisma:migrate:status:game": "prisma migrate status --schema prisma/game.prisma", "prisma:db:push:game": "prisma db push --schema prisma/game.prisma", "prisma:db:push:gateway": "prisma db push --schema prisma/gateway.prisma" }, diff --git a/packages/infra/prisma/migrations/20250101000000_init_game_schema/migration.sql b/packages/infra/prisma/migrations/20250101000000_init_game_schema/migration.sql new file mode 100644 index 0000000..d634e9e --- /dev/null +++ b/packages/infra/prisma/migrations/20250101000000_init_game_schema/migration.sql @@ -0,0 +1,251 @@ +-- Baseline tables that predate the incremental Prisma migration history. +-- +-- IF NOT EXISTS is intentional: existing installations were originally +-- provisioned with `prisma db push` and already own these objects. This +-- migration must be safe both for those databases and for a genuinely empty +-- database running the full migration chain. + +DO $$ +BEGIN + CREATE TYPE "LogScope" AS ENUM ('SYSTEM', 'NATION', 'GENERAL', 'USER'); +EXCEPTION + WHEN duplicate_object THEN NULL; +END +$$; + +DO $$ +BEGIN + CREATE TYPE "LogCategory" AS ENUM ( + 'HISTORY', + 'SUMMARY', + 'ACTION', + 'BATTLE_BRIEF', + 'BATTLE_DETAIL', + 'USER' + ); +EXCEPTION + WHEN duplicate_object THEN NULL; +END +$$; + +CREATE TABLE IF NOT EXISTS "world_state" ( + "id" SERIAL NOT NULL, + "scenario_code" TEXT NOT NULL, + "current_year" INTEGER NOT NULL, + "current_month" INTEGER NOT NULL, + "tick_seconds" INTEGER NOT NULL, + "config" JSONB NOT NULL DEFAULT '{}'::jsonb, + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "world_state_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "nation" ( + "id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL, + "capital_city_id" INTEGER, + "gold" INTEGER NOT NULL DEFAULT 0, + "rice" INTEGER NOT NULL DEFAULT 0, + "tech" DOUBLE PRECISION NOT NULL DEFAULT 0, + "level" INTEGER NOT NULL DEFAULT 0, + "type_code" TEXT NOT NULL DEFAULT 'che_중립', + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + + CONSTRAINT "nation_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "city" ( + "id" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "level" INTEGER NOT NULL, + "nation_id" INTEGER NOT NULL DEFAULT 0, + "supply_state" INTEGER NOT NULL DEFAULT 1, + "front_state" INTEGER NOT NULL DEFAULT 0, + "pop" INTEGER NOT NULL, + "pop_max" INTEGER NOT NULL, + "agri" INTEGER NOT NULL, + "agri_max" INTEGER NOT NULL, + "comm" INTEGER NOT NULL, + "comm_max" INTEGER NOT NULL, + "secu" INTEGER NOT NULL, + "secu_max" INTEGER NOT NULL, + "trust" INTEGER NOT NULL DEFAULT 0, + "trade" INTEGER NOT NULL DEFAULT 100, + "def" INTEGER NOT NULL, + "def_max" INTEGER NOT NULL, + "wall" INTEGER NOT NULL, + "wall_max" INTEGER NOT NULL, + "region" INTEGER NOT NULL, + "conflict" JSONB NOT NULL DEFAULT '{}'::jsonb, + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + + CONSTRAINT "city_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "general" ( + "id" INTEGER NOT NULL, + "user_id" TEXT, + "name" TEXT NOT NULL, + "nation_id" INTEGER NOT NULL DEFAULT 0, + "city_id" INTEGER NOT NULL DEFAULT 0, + "troop_id" INTEGER NOT NULL DEFAULT 0, + "npc_state" INTEGER NOT NULL DEFAULT 0, + "affinity" INTEGER, + "born_year" INTEGER NOT NULL DEFAULT 180, + "dead_year" INTEGER NOT NULL DEFAULT 300, + "picture" TEXT, + "image_server" INTEGER NOT NULL DEFAULT 0, + "leadership" INTEGER NOT NULL DEFAULT 50, + "strength" INTEGER NOT NULL DEFAULT 50, + "intel" INTEGER NOT NULL DEFAULT 50, + "injury" INTEGER NOT NULL DEFAULT 0, + "experience" INTEGER NOT NULL DEFAULT 0, + "dedication" INTEGER NOT NULL DEFAULT 0, + "officer_level" INTEGER NOT NULL DEFAULT 0, + "gold" INTEGER NOT NULL DEFAULT 1000, + "rice" INTEGER NOT NULL DEFAULT 1000, + "crew" INTEGER NOT NULL DEFAULT 0, + "crew_type_id" INTEGER NOT NULL DEFAULT 0, + "train" INTEGER NOT NULL DEFAULT 0, + "atmos" INTEGER NOT NULL DEFAULT 0, + "weapon_code" TEXT NOT NULL DEFAULT 'None', + "book_code" TEXT NOT NULL DEFAULT 'None', + "horse_code" TEXT NOT NULL DEFAULT 'None', + "item_code" TEXT NOT NULL DEFAULT 'None', + "turn_time" TIMESTAMP(3) NOT NULL, + "recent_war_time" TIMESTAMP(3), + "age" INTEGER NOT NULL DEFAULT 20, + "start_age" INTEGER NOT NULL DEFAULT 20, + "personal_code" TEXT NOT NULL DEFAULT 'None', + "special_code" TEXT NOT NULL DEFAULT 'None', + "special2_code" TEXT NOT NULL DEFAULT 'None', + "last_turn" JSONB NOT NULL DEFAULT '{}'::jsonb, + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + "penalty" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "general_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "message" ( + "id" SERIAL NOT NULL, + "mailbox" INTEGER NOT NULL, + "type" TEXT NOT NULL, + "src" INTEGER NOT NULL, + "dest" INTEGER NOT NULL, + "time" TIMESTAMP(3) NOT NULL, + "valid_until" TIMESTAMP(3) NOT NULL, + "message" JSONB NOT NULL, + + CONSTRAINT "message_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "troop" ( + "troop_leader" INTEGER NOT NULL, + "nation" INTEGER NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "troop_pkey" PRIMARY KEY ("troop_leader") +); + +CREATE TABLE IF NOT EXISTS "general_turn" ( + "id" SERIAL NOT NULL, + "general_id" INTEGER NOT NULL, + "turn_idx" INTEGER NOT NULL, + "action_code" TEXT NOT NULL, + "arg" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "general_turn_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX IF NOT EXISTS "general_turn_general_id_turn_idx_key" + ON "general_turn"("general_id", "turn_idx"); + +CREATE TABLE IF NOT EXISTS "nation_turn" ( + "id" SERIAL NOT NULL, + "nation_id" INTEGER NOT NULL, + "officer_level" INTEGER NOT NULL, + "turn_idx" INTEGER NOT NULL, + "action_code" TEXT NOT NULL, + "arg" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "nation_turn_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX IF NOT EXISTS "nation_turn_nation_id_officer_level_turn_idx_key" + ON "nation_turn"("nation_id", "officer_level", "turn_idx"); + +CREATE TABLE IF NOT EXISTS "diplomacy" ( + "id" SERIAL NOT NULL, + "src_nation_id" INTEGER NOT NULL, + "dest_nation_id" INTEGER NOT NULL, + "state_code" INTEGER NOT NULL, + "term" INTEGER NOT NULL DEFAULT 0, + "is_dead" BOOLEAN NOT NULL DEFAULT false, + "is_showing" BOOLEAN NOT NULL DEFAULT true, + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "diplomacy_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX IF NOT EXISTS "diplomacy_src_nation_id_dest_nation_id_key" + ON "diplomacy"("src_nation_id", "dest_nation_id"); + +CREATE TABLE IF NOT EXISTS "event" ( + "id" SERIAL NOT NULL, + "target_code" TEXT NOT NULL, + "priority" INTEGER NOT NULL DEFAULT 0, + "condition" JSONB NOT NULL DEFAULT '{}'::jsonb, + "action" JSONB NOT NULL DEFAULT '{}'::jsonb, + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "event_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE IF NOT EXISTS "log_entry" ( + "id" SERIAL NOT NULL, + "scope" "LogScope" NOT NULL, + "category" "LogCategory" NOT NULL, + "sub_type" TEXT, + "year" INTEGER NOT NULL, + "month" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "general_id" INTEGER, + "nation_id" INTEGER, + "user_id" INTEGER, + "meta" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "log_entry_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "log_entry_scope_category_id_idx" + ON "log_entry"("scope", "category", "id"); +CREATE INDEX IF NOT EXISTS "log_entry_general_id_category_id_idx" + ON "log_entry"("general_id", "category", "id"); +CREATE INDEX IF NOT EXISTS "log_entry_nation_id_category_id_idx" + ON "log_entry"("nation_id", "category", "id"); +CREATE INDEX IF NOT EXISTS "log_entry_user_id_category_id_idx" + ON "log_entry"("user_id", "category", "id"); + +CREATE TABLE IF NOT EXISTS "error_log" ( + "id" SERIAL NOT NULL, + "category" TEXT NOT NULL, + "source" TEXT, + "message" TEXT NOT NULL, + "trace" TEXT, + "context" JSONB NOT NULL DEFAULT '{}'::jsonb, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "error_log_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "error_log_category_id_idx" + ON "error_log"("category", "id"); diff --git a/packages/infra/prisma/migrations/20260726191000_add_auction_bid_meta/migration.sql b/packages/infra/prisma/migrations/20260726191000_add_auction_bid_meta/migration.sql new file mode 100644 index 0000000..b49894c --- /dev/null +++ b/packages/infra/prisma/migrations/20260726191000_add_auction_bid_meta/migration.sql @@ -0,0 +1,5 @@ +-- The Prisma model and auction runtime persist bid metadata, but the original +-- auction migration predates that field. Existing db-push installations may +-- already have it, so keep this reconciliation migration idempotent. +ALTER TABLE "auction_bid" + ADD COLUMN IF NOT EXISTS "meta" JSONB NOT NULL DEFAULT '{}'::jsonb; diff --git a/packages/infra/prisma/migrations/README.md b/packages/infra/prisma/migrations/README.md new file mode 100644 index 0000000..0d2387b --- /dev/null +++ b/packages/infra/prisma/migrations/README.md @@ -0,0 +1,38 @@ +# Game database migrations + +`game.prisma`의 운영·검증 DB는 이 디렉터리의 migration chain으로 +준비한다. 새 환경을 `prisma db push`만으로 만들지 않는다. + +## 적용 + +Git에서 제외된 환경 파일 또는 secret 주입으로 `DATABASE_URL`을 설정한 뒤 +저장소 루트에서 실행한다. + +```bash +pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game +pnpm --filter @sammo-ts/infra prisma:migrate:status:game +``` + +`20250101000000_init_game_schema`는 증분 migration 이력이 생기기 전에 +`db push`로 제공되던 핵심 game table의 baseline이다. 기존 설치에는 +이미 같은 object가 있으므로 type, table과 index 생성은 idempotent하게 +작성했다. 새 migration은 이 baseline 또는 기존 migration 파일을 +수정하지 말고 뒤에 추가한다. + +## 빈 DB 검증 + +다른 개발 DB나 volume을 재사용하지 말고 전용 임시 PostgreSQL database를 +만든다. 위 deploy 명령을 한 번 실행해 전체 migration이 성공하는지, +두 번째 실행이 `No pending migrations to apply`인지 확인한다. 최소한 +다음도 확인한다. + +- `_prisma_migrations`의 모든 행이 완료 상태 +- `world_state`, `nation`, `city`, `general`, `message`, `troop`, + `general_turn`, `nation_turn`, `diplomacy`, `event`, `log_entry`, + `error_log` 존재 +- `nation.chief_general_id` 존재 +- `city.trade` nullable, `city.trust` REAL +- `auction_bid.meta` JSONB, NOT NULL + +검증 뒤에는 이름을 확인한 임시 database와 role만 제거한다. 공유 개발 +database나 Compose volume을 삭제하지 않는다.