문서 추가: 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 entities and DB schema overview: `docs/architecture/legacy-entities.md`
|
||||||
- Legacy engine map: `docs/architecture/legacy-engine.md`
|
- Legacy engine map: `docs/architecture/legacy-engine.md`
|
||||||
|
- Postgres schema proposal (rewrite): `docs/architecture/postgres-schema.md`
|
||||||
|
|
||||||
## Cross-Cutting Policies
|
## 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