문서 추가: PostgreSQL 스키마 제안서 작성 및 메타 필드 정책 설명
This commit is contained in:
@@ -33,6 +33,7 @@ monorepo plan is prepared alongside it.
|
||||
|
||||
- Legacy entities and DB schema overview: `docs/architecture/legacy-entities.md`
|
||||
- Legacy engine map: `docs/architecture/legacy-engine.md`
|
||||
- Postgres schema proposal (rewrite): `docs/architecture/postgres-schema.md`
|
||||
|
||||
## Cross-Cutting Policies
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
# Postgres Schema Proposal (sammo-ts)
|
||||
|
||||
This document proposes a PostgreSQL schema baseline for the TypeScript rewrite.
|
||||
It keeps legacy semantics while enabling flexible growth through JSONB `meta`
|
||||
fields. Use this as the starting point for Prisma models and migrations.
|
||||
|
||||
## Design Principles
|
||||
|
||||
- Prefer explicit columns for hot paths and indexed filters.
|
||||
- Use JSONB `meta` for infrequently queried, extensible, or experimental data.
|
||||
- Keep JSONB values as objects (not arrays or scalars).
|
||||
- Use partial or expression indexes when a JSONB key becomes a hot filter.
|
||||
- If multiple server profiles live in one database, add `server_id` to every
|
||||
table and index it; otherwise, use one database per profile.
|
||||
|
||||
## Meta Policy (Reset)
|
||||
|
||||
- The rewrite resets naming: legacy `aux` is now `meta`.
|
||||
- `meta` is the default extension surface for each entity.
|
||||
- Keys with explicit enum contracts (ex: `NationAuxKey`) should live in a
|
||||
dedicated table or typed column, not inside `meta`.
|
||||
- When a key transitions into a hot query/filter, promote it to a column or
|
||||
`*_flags` table and keep the old value mirrored during migration.
|
||||
|
||||
## Core Tables (DDL Sketch)
|
||||
|
||||
```sql
|
||||
-- users: account-level identity (game servers can share this table)
|
||||
CREATE TABLE app_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
login_id TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
last_login_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- world_state: per-server runtime state and config
|
||||
CREATE TABLE world_state (
|
||||
id SMALLSERIAL PRIMARY KEY,
|
||||
scenario_code TEXT NOT NULL,
|
||||
current_year SMALLINT NOT NULL,
|
||||
current_month SMALLINT NOT NULL,
|
||||
tick_seconds INTEGER NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (jsonb_typeof(config) = 'object'),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE nation (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
capital_city_id BIGINT,
|
||||
gold INTEGER NOT NULL DEFAULT 0,
|
||||
rice INTEGER NOT NULL DEFAULT 0,
|
||||
bill SMALLINT NOT NULL DEFAULT 0,
|
||||
rate SMALLINT NOT NULL DEFAULT 0,
|
||||
rate_tmp SMALLINT NOT NULL DEFAULT 0,
|
||||
secret_limit SMALLINT NOT NULL DEFAULT 3,
|
||||
chief_general_id BIGINT NOT NULL DEFAULT 0,
|
||||
scout_level SMALLINT NOT NULL DEFAULT 0,
|
||||
war_state SMALLINT NOT NULL DEFAULT 0,
|
||||
strategic_cmd_limit SMALLINT NOT NULL DEFAULT 36,
|
||||
surrender_limit SMALLINT NOT NULL DEFAULT 72,
|
||||
tech REAL NOT NULL DEFAULT 0,
|
||||
power INTEGER NOT NULL DEFAULT 0,
|
||||
level SMALLINT NOT NULL DEFAULT 0,
|
||||
type_code TEXT NOT NULL DEFAULT 'che_중립',
|
||||
spy JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (jsonb_typeof(spy) = 'object'),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE city (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
level SMALLINT NOT NULL,
|
||||
nation_id BIGINT NOT NULL DEFAULT 0,
|
||||
supply_state SMALLINT NOT NULL DEFAULT 1,
|
||||
front_state SMALLINT 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,
|
||||
dead SMALLINT NOT NULL DEFAULT 0,
|
||||
def INTEGER NOT NULL DEFAULT 0,
|
||||
def_max INTEGER NOT NULL DEFAULT 0,
|
||||
wall INTEGER NOT NULL DEFAULT 0,
|
||||
wall_max INTEGER NOT NULL DEFAULT 0,
|
||||
officer_set INTEGER NOT NULL DEFAULT 0,
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
region SMALLINT NOT NULL DEFAULT 0,
|
||||
term SMALLINT NOT NULL DEFAULT 0,
|
||||
conflict JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
CHECK (jsonb_typeof(conflict) = 'object'),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE troop (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
leader_general_id BIGINT NOT NULL,
|
||||
nation_id BIGINT NOT NULL DEFAULT 0,
|
||||
name TEXT NOT NULL,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE general (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
name TEXT NOT NULL,
|
||||
nation_id BIGINT NOT NULL DEFAULT 0,
|
||||
city_id BIGINT NOT NULL DEFAULT 0,
|
||||
troop_id BIGINT NOT NULL DEFAULT 0,
|
||||
npc_state SMALLINT NOT NULL DEFAULT 0,
|
||||
affinity SMALLINT NOT NULL DEFAULT 0,
|
||||
born_year SMALLINT NOT NULL DEFAULT 180,
|
||||
dead_year SMALLINT NOT NULL DEFAULT 300,
|
||||
picture TEXT NOT NULL,
|
||||
image_server SMALLINT NOT NULL DEFAULT 0,
|
||||
leadership SMALLINT NOT NULL DEFAULT 50,
|
||||
leadership_exp SMALLINT NOT NULL DEFAULT 0,
|
||||
strength SMALLINT NOT NULL DEFAULT 50,
|
||||
strength_exp SMALLINT NOT NULL DEFAULT 0,
|
||||
intel SMALLINT NOT NULL DEFAULT 50,
|
||||
intel_exp SMALLINT NOT NULL DEFAULT 0,
|
||||
injury SMALLINT NOT NULL DEFAULT 0,
|
||||
experience INTEGER NOT NULL DEFAULT 0,
|
||||
dedication INTEGER NOT NULL DEFAULT 0,
|
||||
officer_level SMALLINT NOT NULL DEFAULT 0,
|
||||
officer_city INTEGER NOT NULL DEFAULT 0,
|
||||
permission TEXT NOT NULL DEFAULT 'normal',
|
||||
gold INTEGER NOT NULL DEFAULT 1000,
|
||||
rice INTEGER NOT NULL DEFAULT 1000,
|
||||
crew INTEGER NOT NULL DEFAULT 0,
|
||||
crew_type SMALLINT NOT NULL DEFAULT 0,
|
||||
train SMALLINT NOT NULL DEFAULT 0,
|
||||
atmos SMALLINT 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 TIMESTAMPTZ NOT NULL,
|
||||
recent_war_time TIMESTAMPTZ,
|
||||
make_limit SMALLINT NOT NULL DEFAULT 0,
|
||||
kill_turn SMALLINT,
|
||||
block_state SMALLINT NOT NULL DEFAULT 0,
|
||||
ded_level SMALLINT NOT NULL DEFAULT 0,
|
||||
exp_level SMALLINT NOT NULL DEFAULT 0,
|
||||
age SMALLINT NOT NULL DEFAULT 20,
|
||||
start_age SMALLINT NOT NULL DEFAULT 20,
|
||||
belong SMALLINT NOT NULL DEFAULT 1,
|
||||
betray SMALLINT NOT NULL DEFAULT 0,
|
||||
personal_code TEXT NOT NULL DEFAULT 'None',
|
||||
special_code TEXT NOT NULL DEFAULT 'None',
|
||||
spec_age SMALLINT NOT NULL DEFAULT 0,
|
||||
special2_code TEXT NOT NULL DEFAULT 'None',
|
||||
spec2_age SMALLINT NOT NULL DEFAULT 0,
|
||||
defence_train SMALLINT NOT NULL DEFAULT 80,
|
||||
tournament_state SMALLINT NOT NULL DEFAULT 0,
|
||||
last_turn JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
penalty JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (jsonb_typeof(last_turn) = 'object'),
|
||||
CHECK (jsonb_typeof(meta) = 'object'),
|
||||
CHECK (jsonb_typeof(penalty) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE general_turn (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
general_id BIGINT NOT NULL REFERENCES general(id) ON DELETE CASCADE,
|
||||
turn_idx SMALLINT NOT NULL,
|
||||
action_code TEXT NOT NULL,
|
||||
arg JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
brief TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (general_id, turn_idx),
|
||||
CHECK (jsonb_typeof(arg) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE nation_turn (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nation_id BIGINT NOT NULL REFERENCES nation(id) ON DELETE CASCADE,
|
||||
officer_level SMALLINT NOT NULL,
|
||||
turn_idx SMALLINT NOT NULL,
|
||||
action_code TEXT NOT NULL,
|
||||
arg JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
brief TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (nation_id, officer_level, turn_idx),
|
||||
CHECK (jsonb_typeof(arg) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE diplomacy (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
src_nation_id BIGINT NOT NULL,
|
||||
dest_nation_id BIGINT NOT NULL,
|
||||
state_code TEXT NOT NULL,
|
||||
term SMALLINT 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 TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE message (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
mailbox_code TEXT NOT NULL,
|
||||
message_type TEXT NOT NULL,
|
||||
src_id BIGINT,
|
||||
dest_id BIGINT,
|
||||
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
valid_until TIMESTAMPTZ,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
CHECK (jsonb_typeof(payload) = 'object'),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
|
||||
CREATE TABLE event (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
target_code TEXT NOT NULL,
|
||||
priority SMALLINT NOT NULL DEFAULT 0,
|
||||
condition JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
action JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (jsonb_typeof(condition) = 'object'),
|
||||
CHECK (jsonb_typeof(action) = 'object'),
|
||||
CHECK (jsonb_typeof(meta) = 'object')
|
||||
);
|
||||
```
|
||||
|
||||
## Meta Indexing Examples
|
||||
|
||||
```sql
|
||||
-- Generic meta GIN index (use jsonb_path_ops when keys are simple)
|
||||
CREATE INDEX idx_general_meta_gin ON general USING GIN (meta jsonb_path_ops);
|
||||
|
||||
-- Hot key promotion: when a key becomes a frequent filter
|
||||
CREATE INDEX idx_general_meta_trait ON general ((meta ->> 'trait_code'));
|
||||
|
||||
-- JSONB object safety (optional additional guardrails)
|
||||
ALTER TABLE general ADD CONSTRAINT general_meta_object CHECK (jsonb_typeof(meta) = 'object');
|
||||
```
|
||||
|
||||
## `NationAuxKey` Handling
|
||||
|
||||
Legacy `NationAuxKey` is an explicit enum contract. Do not store these keys in
|
||||
`nation.meta`. Use a dedicated table or typed column instead.
|
||||
|
||||
```sql
|
||||
CREATE TYPE nation_aux_key AS ENUM (
|
||||
'can_국기변경',
|
||||
'can_국호변경',
|
||||
'did_특성초토화',
|
||||
'can_무작위수도이전',
|
||||
'can_대검병사용',
|
||||
'can_극병사용',
|
||||
'can_화시병사용',
|
||||
'can_원융노병사용',
|
||||
'can_산저병사용',
|
||||
'can_상병사용',
|
||||
'can_음귀병사용',
|
||||
'can_무희사용',
|
||||
'can_화륜차사용'
|
||||
);
|
||||
|
||||
CREATE TABLE nation_flag (
|
||||
nation_id BIGINT NOT NULL REFERENCES nation(id) ON DELETE CASCADE,
|
||||
key nation_aux_key NOT NULL,
|
||||
value JSONB NOT NULL DEFAULT 'true'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (nation_id, key)
|
||||
);
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Legacy `aux` is renamed to `meta` in the rewrite; migrate values 1:1.
|
||||
- `meta`, `last_turn`, `penalty`, `arg`, and message payloads map naturally to
|
||||
JSONB. Keep them as objects to ease validations and reuse existing TS RNG
|
||||
tooling without ad-hoc randomness.
|
||||
- Favor deterministic keys in JSONB for reproducible action logs and turn
|
||||
outcomes.
|
||||
- If a JSONB key starts affecting query plans or UI filters, promote it to a
|
||||
typed column and keep a backfill step in the migration.
|
||||
Reference in New Issue
Block a user